Merge "Use a fake in-memory implementation for AccountPatchReviewStore in tests"
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index ba20cea..5ff9425 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -246,6 +246,15 @@
 name contains the file pattern, or the edits of the file diff contain the edit
 pattern.
 
+[[operator_label]]
+label:labelName=+1,user=non_contributor::
++
+Submit requirements support an additional `user=non_contributor` argument for
+labels that returns true if the change has a label vote matching the specified
+value and the vote is applied from a gerrit account that's not the uploader,
+author or committer of the latest patchset. See the documentation for the labels
+operator in the link:user-search.html[user search] page.
+
 [[unsupported_operators]]
 === Unsupported Operators
 
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 40470a6..d4a1864 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -357,6 +357,22 @@
 
 Submit any previously uploaded notes change on the homepage project.
 
+[[update-supported-releases]]
+=== Update list of supported releases
+
+If you created a new stable release update the list of supported releases
+in the link:https://www.gerritcodereview.com/support.html[support page].
+
+Gerrit releases are also listed on the
+link:https://endoflife.date/gerrit[endoflife website].
+Push a PR to
+link:https://github.com/endoflife-date/endoflife.date.git[endoflife.date repository]
+to update supported releases in `products/gerrit.md`. New release tags
+should be updated automatically by the site's automation job which uses
+Dependabot to
+link:https://github.com/endoflife-date/endoflife.date/wiki/Automation[auto-create PRs]
+for new release tags.
+
 [[finalize-issues]]
 ==== Update the Issues
 
diff --git a/contrib/git-gc-preserve b/contrib/git-gc-preserve
index a886721..33c8f5b 100755
--- a/contrib/git-gc-preserve
+++ b/contrib/git-gc-preserve
@@ -49,10 +49,10 @@
     [2] https://git.eclipse.org/r/c/jgit/jgit/+/122288
 
 CONFIGURATION
-    "gc.prunepreserved": if set to "true" preserved packs from the last gc run
+    "pack.prunepreserved": if set to "true" preserved packs from the last gc run
       are pruned before current packs are preserved.
 
-    "gc.preserveoldpacks": if set to "true" current packs will be hard linked
+    "pack.preserveoldpacks": if set to "true" current packs will be hard linked
       to objects/pack/preserved before git gc is executed. JGit will
       fallback to the preserved packs in this directory in case it comes
       across missing objects which might be caused by a concurrent run of
@@ -84,9 +84,9 @@
   exec 9>&-
 }
 
-# prune preserved packs if gc.prunepreserved == true
+# prune preserved packs if pack.prunepreserved == true
 prune_preserved() { # repo
-  configured=$(git --git-dir="$1" config --get gc.prunepreserved)
+  configured=$(git --git-dir="$1" config --get pack.prunepreserved)
   if [ "$configured" != "true" ]; then
     return 0
   fi
@@ -99,9 +99,9 @@
   fi
 }
 
-# preserve packs if gc.preserveoldpacks == true
+# preserve packs if pack.preserveoldpacks == true
 preserve_packs() { # repo
-  configured=$(git --git-dir="$1" config --get gc.preserveoldpacks)
+  configured=$(git --git-dir="$1" config --get pack.preserveoldpacks)
   if [ "$configured" != "true" ]; then
     return 0
   fi
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index a4f973e..e93c152 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -33,6 +33,7 @@
 import static com.google.gerrit.server.project.testing.TestLabels.label;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
@@ -1707,11 +1708,14 @@
     }
 
     public void save() throws Exception {
-      metaDataUpdate.setAuthor(identifiedUserFactory.create(admin.id()));
-      projectConfig.commit(metaDataUpdate);
-      metaDataUpdate.close();
-      metaDataUpdate = null;
-      projectCache.evictAndReindex(projectConfig.getProject());
+      testRefAction(
+          () -> {
+            metaDataUpdate.setAuthor(identifiedUserFactory.create(admin.id()));
+            projectConfig.commit(metaDataUpdate);
+            metaDataUpdate.close();
+            metaDataUpdate = null;
+            projectCache.evictAndReindex(projectConfig.getProject());
+          });
     }
 
     @Override
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 3c7ec2b..8b2160c 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -53,6 +53,7 @@
     "//lib/bouncycastle:bcpg",
     "//lib/bouncycastle:bcpkix",
     "//lib/bouncycastle:bcprov",
+    "//lib/bouncycastle:bcutil",
     "//prolog:gerrit-prolog-common",
 ]
 
@@ -75,6 +76,7 @@
     "//java/com/google/gerrit/gpg/testing:gpg-test-util",
     "//java/com/google/gerrit/git/testing",
     "//java/com/google/gerrit/index/testing",
+    "//java/com/google/gerrit/testing:test-ref-update-context",
     "//lib/errorprone:annotations",
 ]
 
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index 46f7496..c8ab1a9 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.entities.RefNames.REFS_USERS;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ImmutableList;
@@ -202,10 +203,12 @@
     keptRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
     restoredRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
     deletedRefsByProject = MultimapBuilder.hashKeys().arrayListValues().build();
-
-    restoreRefs();
-    deleteNewlyCreatedRefs();
-    evictCachesAndReindex();
+    testRefAction(
+        () -> {
+          restoreRefs();
+          deleteNewlyCreatedRefs();
+          evictCachesAndReindex();
+        });
   }
 
   /** Read the states of all matching refs. */
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
index 277d219..e510ba3 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.testsuite.account;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.US_ASCII;
 
 import com.google.gerrit.acceptance.SshEnabled;
@@ -88,7 +89,8 @@
   private KeyPair createKeyPair(Account.Id accountId, String username, @Nullable String email)
       throws Exception {
     KeyPair keyPair = SshSessionFactory.genSshKey();
-    authorizedKeys.addKey(accountId, publicKey(keyPair, email));
+    testRefAction(() -> authorizedKeys.addKey(accountId, publicKey(keyPair, email)));
+
     sshKeyCache.evict(username);
     return keyPair;
   }
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
index 62ad7c4..1dcd662 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -44,6 +45,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -132,30 +134,32 @@
   }
 
   private Change.Id createChange(TestChangeCreation changeCreation) throws Exception {
-    Change.Id changeId = Change.id(seq.nextChangeId());
-    Project.NameKey project = getTargetProject(changeCreation);
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      Change.Id changeId = Change.id(seq.nextChangeId());
+      Project.NameKey project = getTargetProject(changeCreation);
 
-    try (Repository repository = repositoryManager.openRepository(project);
-        ObjectInserter objectInserter = repository.newObjectInserter();
-        RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-      Instant now = TimeUtil.now();
-      IdentifiedUser changeOwner = getChangeOwner(changeCreation);
-      PersonIdent author = getAuthorIdent(now, changeCreation);
-      PersonIdent committer = getCommitterIdent(now, changeCreation);
-      ObjectId commitId =
-          createCommit(repository, revWalk, objectInserter, changeCreation, author, committer);
+      try (Repository repository = repositoryManager.openRepository(project);
+          ObjectInserter objectInserter = repository.newObjectInserter();
+          RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+        Instant now = TimeUtil.now();
+        IdentifiedUser changeOwner = getChangeOwner(changeCreation);
+        PersonIdent author = getAuthorIdent(now, changeCreation);
+        PersonIdent committer = getCommitterIdent(now, changeCreation);
+        ObjectId commitId =
+            createCommit(repository, revWalk, objectInserter, changeCreation, author, committer);
 
-      String refName = RefNames.fullName(changeCreation.branch());
-      ChangeInserter inserter = getChangeInserter(changeId, refName, commitId);
-      changeCreation.topic().ifPresent(t -> inserter.setTopic(t));
-      inserter.setApprovals(changeCreation.approvals());
+        String refName = RefNames.fullName(changeCreation.branch());
+        ChangeInserter inserter = getChangeInserter(changeId, refName, commitId);
+        changeCreation.topic().ifPresent(t -> inserter.setTopic(t));
+        inserter.setApprovals(changeCreation.approvals());
 
-      try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
-        batchUpdate.setRepository(repository, revWalk, objectInserter);
-        batchUpdate.insertChange(inserter);
-        batchUpdate.execute();
+        try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
+          batchUpdate.setRepository(repository, revWalk, objectInserter);
+          batchUpdate.insertChange(inserter);
+          batchUpdate.execute();
+        }
+        return changeId;
       }
-      return changeId;
     }
   }
 
@@ -452,39 +456,41 @@
 
     private PatchSet.Id createPatchset(TestPatchsetCreation patchsetCreation)
         throws IOException, RestApiException, UpdateException {
-      ChangeNotes changeNotes = getChangeNotes();
-      Project.NameKey project = changeNotes.getProjectName();
-      try (Repository repository = repositoryManager.openRepository(project);
-          ObjectInserter objectInserter = repository.newObjectInserter();
-          RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-        Instant now = TimeUtil.now();
-        PersonIdent authorIdent = getAuthorIdent(now, patchsetCreation);
-        PersonIdent committerIdent = getCommitterIdent(now, patchsetCreation);
-        ObjectId newPatchsetCommit =
-            createPatchsetCommit(
-                repository,
-                revWalk,
-                objectInserter,
-                changeNotes,
-                patchsetCreation,
-                authorIdent,
-                committerIdent,
-                now);
+      try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+        ChangeNotes changeNotes = getChangeNotes();
+        Project.NameKey project = changeNotes.getProjectName();
+        try (Repository repository = repositoryManager.openRepository(project);
+            ObjectInserter objectInserter = repository.newObjectInserter();
+            RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+          Instant now = TimeUtil.now();
+          PersonIdent authorIdent = getAuthorIdent(now, patchsetCreation);
+          PersonIdent committerIdent = getCommitterIdent(now, patchsetCreation);
+          ObjectId newPatchsetCommit =
+              createPatchsetCommit(
+                  repository,
+                  revWalk,
+                  objectInserter,
+                  changeNotes,
+                  patchsetCreation,
+                  authorIdent,
+                  committerIdent,
+                  now);
 
-        PatchSet.Id patchsetId =
-            ChangeUtil.nextPatchSetId(repository, changeNotes.getCurrentPatchSet().id());
-        PatchSetInserter patchSetInserter =
-            getPatchSetInserter(changeNotes, newPatchsetCommit, patchsetId);
+          PatchSet.Id patchsetId =
+              ChangeUtil.nextPatchSetId(repository, changeNotes.getCurrentPatchSet().id());
+          PatchSetInserter patchSetInserter =
+              getPatchSetInserter(changeNotes, newPatchsetCommit, patchsetId);
 
-        Account.Id uploaderId =
-            patchsetCreation.uploader().orElse(changeNotes.getChange().getOwner());
-        IdentifiedUser uploader = userFactory.create(uploaderId);
-        try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, uploader, now)) {
-          batchUpdate.setRepository(repository, revWalk, objectInserter);
-          batchUpdate.addOp(changeId, patchSetInserter);
-          batchUpdate.execute();
+          Account.Id uploaderId =
+              patchsetCreation.uploader().orElse(changeNotes.getChange().getOwner());
+          IdentifiedUser uploader = userFactory.create(uploaderId);
+          try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, uploader, now)) {
+            batchUpdate.setRepository(repository, revWalk, objectInserter);
+            batchUpdate.addOp(changeId, patchSetInserter);
+            batchUpdate.execute();
+          }
+          return patchsetId;
         }
-        return patchsetId;
       }
     }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
index 9b393ef..3bd355b 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.acceptance.testsuite.change;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
+
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Comment.Status;
 import com.google.gerrit.entities.HumanComment;
@@ -34,6 +36,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -101,21 +104,23 @@
 
   private String createComment(TestCommentCreation commentCreation)
       throws IOException, RestApiException, UpdateException {
+
     Project.NameKey project = changeNotes.getProjectName();
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (Repository repository = repositoryManager.openRepository(project);
+          ObjectInserter objectInserter = repository.newObjectInserter();
+          RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+        Instant now = TimeUtil.now();
 
-    try (Repository repository = repositoryManager.openRepository(project);
-        ObjectInserter objectInserter = repository.newObjectInserter();
-        RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-      Instant now = TimeUtil.now();
-
-      IdentifiedUser author = getAuthor(commentCreation);
-      CommentAdditionOp commentAdditionOp = new CommentAdditionOp(commentCreation);
-      try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) {
-        batchUpdate.setRepository(repository, revWalk, objectInserter);
-        batchUpdate.addOp(changeNotes.getChangeId(), commentAdditionOp);
-        batchUpdate.execute();
+        IdentifiedUser author = getAuthor(commentCreation);
+        CommentAdditionOp commentAdditionOp = new CommentAdditionOp(commentCreation);
+        try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) {
+          batchUpdate.setRepository(repository, revWalk, objectInserter);
+          batchUpdate.addOp(changeNotes.getChangeId(), commentAdditionOp);
+          batchUpdate.execute();
+        }
+        return commentAdditionOp.createdCommentUuid;
       }
-      return commentAdditionOp.createdCommentUuid;
     }
   }
 
@@ -197,21 +202,22 @@
   private String createRobotComment(TestRobotCommentCreation robotCommentCreation)
       throws IOException, RestApiException, UpdateException {
     Project.NameKey project = changeNotes.getProjectName();
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (Repository repository = repositoryManager.openRepository(project);
+          ObjectInserter objectInserter = repository.newObjectInserter();
+          RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
+        Instant now = TimeUtil.now();
 
-    try (Repository repository = repositoryManager.openRepository(project);
-        ObjectInserter objectInserter = repository.newObjectInserter();
-        RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-      Instant now = TimeUtil.now();
-
-      IdentifiedUser author = getAuthor(robotCommentCreation);
-      RobotCommentAdditionOp robotCommentAdditionOp =
-          new RobotCommentAdditionOp(robotCommentCreation);
-      try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) {
-        batchUpdate.setRepository(repository, revWalk, objectInserter);
-        batchUpdate.addOp(changeNotes.getChangeId(), robotCommentAdditionOp);
-        batchUpdate.execute();
+        IdentifiedUser author = getAuthor(robotCommentCreation);
+        RobotCommentAdditionOp robotCommentAdditionOp =
+            new RobotCommentAdditionOp(robotCommentCreation);
+        try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, author, now)) {
+          batchUpdate.setRepository(repository, revWalk, objectInserter);
+          batchUpdate.addOp(changeNotes.getChangeId(), robotCommentAdditionOp);
+          batchUpdate.execute();
+        }
+        return robotCommentAdditionOp.createdRobotCommentUuid;
       }
-      return robotCommentAdditionOp.createdRobotCommentUuid;
     }
   }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/BUILD b/java/com/google/gerrit/acceptance/testsuite/group/BUILD
index d4f1175..2052105 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/BUILD
+++ b/java/com/google/gerrit/acceptance/testsuite/group/BUILD
@@ -14,6 +14,7 @@
         "//java/com/google/gerrit/exceptions",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//lib:guava",
         "//lib:jgit",
         "//lib:jgit-junit",
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
index 8bb7b23..99899cf 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroupCreation.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.acceptance.testsuite.group;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
+
 import com.google.auto.value.AutoValue;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -106,7 +108,7 @@
      */
     public AccountGroup.UUID create() {
       TestGroupCreation groupCreation = autoBuild();
-      return groupCreation.groupCreator().applyAndThrowSilently(groupCreation);
+      return testRefAction(() -> groupCreation.groupCreator().applyAndThrowSilently(groupCreation));
     }
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/BUILD b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
index d34b79a..4ac2705 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/BUILD
+++ b/java/com/google/gerrit/acceptance/testsuite/project/BUILD
@@ -4,6 +4,7 @@
 
 java_library(
     name = "project",
+    testonly = True,
     srcs = glob(["*.java"]),
     visibility = ["//visibility:public"],
     deps = [
@@ -13,6 +14,7 @@
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/extensions:api",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//lib:guava",
         "//lib:jgit",
         "//lib:jgit-junit",
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index 69139ce..bd3d656 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
 import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCreator;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -136,19 +138,21 @@
 
     private void updateProject(TestProjectUpdate projectUpdate)
         throws IOException, ConfigInvalidException {
-      try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(nameKey)) {
-        ProjectConfig projectConfig = projectConfigFactory.read(metaDataUpdate);
-        if (projectUpdate.removeAllAccessSections()) {
-          projectConfig.getAccessSections().forEach(as -> projectConfig.remove(as));
+      try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+        try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(nameKey)) {
+          ProjectConfig projectConfig = projectConfigFactory.read(metaDataUpdate);
+          if (projectUpdate.removeAllAccessSections()) {
+            projectConfig.getAccessSections().forEach(as -> projectConfig.remove(as));
+          }
+          removePermissions(projectConfig, projectUpdate.removedPermissions());
+          addCapabilities(projectConfig, projectUpdate.addedCapabilities());
+          addPermissions(projectConfig, projectUpdate.addedPermissions());
+          addLabelPermissions(projectConfig, projectUpdate.addedLabelPermissions());
+          setExclusiveGroupPermissions(projectConfig, projectUpdate.exclusiveGroupPermissions());
+          projectConfig.commit(metaDataUpdate);
         }
-        removePermissions(projectConfig, projectUpdate.removedPermissions());
-        addCapabilities(projectConfig, projectUpdate.addedCapabilities());
-        addPermissions(projectConfig, projectUpdate.addedPermissions());
-        addLabelPermissions(projectConfig, projectUpdate.addedLabelPermissions());
-        setExclusiveGroupPermissions(projectConfig, projectUpdate.exclusiveGroupPermissions());
-        projectConfig.commit(metaDataUpdate);
+        projectCache.evictAndReindex(nameKey);
       }
-      projectCache.evictAndReindex(nameKey);
     }
 
     private void removePermissions(
diff --git a/java/com/google/gerrit/entities/RefNames.java b/java/com/google/gerrit/entities/RefNames.java
index 9745fc5..e79c530 100644
--- a/java/com/google/gerrit/entities/RefNames.java
+++ b/java/com/google/gerrit/entities/RefNames.java
@@ -185,6 +185,21 @@
     return ref.startsWith(REFS_CHANGES);
   }
 
+  /** True if the provided ref is in {@code refs/sequences/*}. */
+  public static boolean isSequenceRef(String ref) {
+    return ref.startsWith(REFS_SEQUENCES);
+  }
+
+  /** True if the provided ref is in {@code refs/tags/*}. */
+  public static boolean isTagRef(String ref) {
+    return ref.startsWith(REFS_TAGS);
+  }
+
+  /** True if the provided ref is {@link #REFS_EXTERNAL_IDS}. */
+  public static boolean isExternalIdRef(String ref) {
+    return REFS_EXTERNAL_IDS.equals(ref);
+  }
+
   public static String refsGroups(AccountGroup.UUID groupUuid) {
     return REFS_GROUPS + shardUuid(groupUuid.get());
   }
@@ -330,6 +345,21 @@
     return REFS_CONFIG.equals(ref);
   }
 
+  /** Whether the ref is the version branch, i.e. {@code refs/meta/version}. */
+  public static boolean isVersionRef(String ref) {
+    return REFS_VERSION.equals(ref);
+  }
+
+  /** Whether the ref is an auto-merge ref. */
+  public static boolean isAutoMergeRef(String ref) {
+    return ref.startsWith(REFS_CACHE_AUTOMERGE);
+  }
+
+  /** Whether the ref is an reject commit ref, i.e. {@code refs/meta/reject-commits} */
+  public static boolean isRejectCommitsRef(String ref) {
+    return REFS_REJECT_COMMITS.equals(ref);
+  }
+
   /**
    * Whether the ref is managed by Gerrit. Covers all Gerrit-internal refs like refs/cache-automerge
    * and refs/meta as well as refs/changes. Does not cover user-created refs like branches or custom
diff --git a/java/com/google/gerrit/gpg/PublicKeyStore.java b/java/com/google/gerrit/gpg/PublicKeyStore.java
index d167ac8..cbd2bba 100644
--- a/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.gpg;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GPG_KEYS_MODIFICATION;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -24,6 +25,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.git.ObjectIds;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -377,35 +379,36 @@
       newTip = ins.insert(cb);
       ins.flush();
     }
-
-    RefUpdate ru = repo.updateRef(PublicKeyStore.REFS_GPG_KEYS);
-    ru.setExpectedOldObjectId(tip);
-    ru.setNewObjectId(newTip);
-    ru.setRefLogIdent(cb.getCommitter());
-    ru.setRefLogMessage("Store public keys", true);
-    RefUpdate.Result result = ru.update();
-    reset();
-    switch (result) {
-      case FAST_FORWARD:
-      case NEW:
-      case NO_CHANGE:
-        toAdd.clear();
-        toRemove.clear();
-        break;
-      case LOCK_FAILURE:
-        throw new LockFailureException("Failed to store public keys", ru);
-      case FORCED:
-      case IO_FAILURE:
-      case NOT_ATTEMPTED:
-      case REJECTED:
-      case REJECTED_CURRENT_BRANCH:
-      case RENAMED:
-      case REJECTED_MISSING_OBJECT:
-      case REJECTED_OTHER_REASON:
-      default:
-        break;
+    try (RefUpdateContext ctx = RefUpdateContext.open(GPG_KEYS_MODIFICATION)) {
+      RefUpdate ru = repo.updateRef(PublicKeyStore.REFS_GPG_KEYS);
+      ru.setExpectedOldObjectId(tip);
+      ru.setNewObjectId(newTip);
+      ru.setRefLogIdent(cb.getCommitter());
+      ru.setRefLogMessage("Store public keys", true);
+      RefUpdate.Result result = ru.update();
+      reset();
+      switch (result) {
+        case FAST_FORWARD:
+        case NEW:
+        case NO_CHANGE:
+          toAdd.clear();
+          toRemove.clear();
+          break;
+        case LOCK_FAILURE:
+          throw new LockFailureException("Failed to store public keys", ru);
+        case FORCED:
+        case IO_FAILURE:
+        case NOT_ATTEMPTED:
+        case REJECTED:
+        case REJECTED_CURRENT_BRANCH:
+        case RENAMED:
+        case REJECTED_MISSING_OBJECT:
+        case REJECTED_OTHER_REASON:
+        default:
+          break;
+      }
+      return result;
     }
-    return result;
   }
 
   private void saveToNotes(ObjectInserter ins, PGPPublicKeyRing keyRing)
diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
index 72bfe40..92788b7 100644
--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
@@ -121,7 +121,7 @@
       data.put("userIsAuthenticated", true);
       if (page == RequestedPage.DASHBOARD) {
         data.put("defaultDashboardHex", ListOption.toHex(IndexPreloadingUtil.DASHBOARD_OPTIONS));
-        data.put("dashboardQuery", IndexPreloadingUtil.computeDashboardQueryList(serverApi));
+        data.put("dashboardQuery", IndexPreloadingUtil.computeDashboardQueryList());
       }
     } catch (AuthException e) {
       logger.atFine().log("Can't inline account-related data because user is unauthenticated");
diff --git a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
index bb3b6d5..afaeaf6 100644
--- a/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
+++ b/java/com/google/gerrit/httpd/raw/IndexPreloadingUtil.java
@@ -22,9 +22,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
 import com.google.gerrit.common.UsedAt.Project;
-import com.google.gerrit.extensions.api.config.Server;
 import com.google.gerrit.extensions.client.ListChangesOption;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.Url;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -188,7 +186,7 @@
     return Optional.empty();
   }
 
-  public static List<String> computeDashboardQueryList(Server serverApi) throws RestApiException {
+  public static List<String> computeDashboardQueryList() {
     List<String> queryList = new ArrayList<>();
     queryList.add(SELF_DASHBOARD_HAS_UNPUBLISHED_DRAFTS_QUERY);
     queryList.add(SELF_YOUR_TURN);
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 9403105..2d18054 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.joining;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -388,27 +390,29 @@
       u.setNewObjectId(writeLabels(repo, labels));
       u.setRefLogIdent(serverIdent.get());
       u.setRefLogMessage("Update star labels", true);
-      RefUpdate.Result result = u.update(rw);
-      switch (result) {
-        case NEW:
-        case FORCED:
-        case NO_CHANGE:
-        case FAST_FORWARD:
-          gitRefUpdated.fire(allUsers, u, null);
-          return;
-        case LOCK_FAILURE:
-          throw new LockFailureException(
-              String.format("Update star labels on ref %s failed", refName), u);
-        case IO_FAILURE:
-        case NOT_ATTEMPTED:
-        case REJECTED:
-        case REJECTED_CURRENT_BRANCH:
-        case RENAMED:
-        case REJECTED_MISSING_OBJECT:
-        case REJECTED_OTHER_REASON:
-        default:
-          throw new StorageException(
-              String.format("Update star labels on ref %s failed: %s", refName, result.name()));
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        RefUpdate.Result result = u.update(rw);
+        switch (result) {
+          case NEW:
+          case FORCED:
+          case NO_CHANGE:
+          case FAST_FORWARD:
+            gitRefUpdated.fire(allUsers, u, null);
+            return;
+          case LOCK_FAILURE:
+            throw new LockFailureException(
+                String.format("Update star labels on ref %s failed", refName), u);
+          case IO_FAILURE:
+          case NOT_ATTEMPTED:
+          case REJECTED:
+          case REJECTED_CURRENT_BRANCH:
+          case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
+          default:
+            throw new StorageException(
+                String.format("Update star labels on ref %s failed: %s", refName, result.name()));
+        }
       }
     }
   }
@@ -427,26 +431,28 @@
       u.setExpectedOldObjectId(oldObjectId);
       u.setRefLogIdent(serverIdent.get());
       u.setRefLogMessage("Unstar change", true);
-      RefUpdate.Result result = u.delete();
-      switch (result) {
-        case FORCED:
-          gitRefUpdated.fire(allUsers, u, null);
-          return;
-        case LOCK_FAILURE:
-          throw new LockFailureException(String.format("Delete star ref %s failed", refName), u);
-        case NEW:
-        case NO_CHANGE:
-        case FAST_FORWARD:
-        case IO_FAILURE:
-        case NOT_ATTEMPTED:
-        case REJECTED:
-        case REJECTED_CURRENT_BRANCH:
-        case RENAMED:
-        case REJECTED_MISSING_OBJECT:
-        case REJECTED_OTHER_REASON:
-        default:
-          throw new StorageException(
-              String.format("Delete star ref %s failed: %s", refName, result.name()));
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        RefUpdate.Result result = u.delete();
+        switch (result) {
+          case FORCED:
+            gitRefUpdated.fire(allUsers, u, null);
+            return;
+          case LOCK_FAILURE:
+            throw new LockFailureException(String.format("Delete star ref %s failed", refName), u);
+          case NEW:
+          case NO_CHANGE:
+          case FAST_FORWARD:
+          case IO_FAILURE:
+          case NOT_ATTEMPTED:
+          case REJECTED:
+          case REJECTED_CURRENT_BRANCH:
+          case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
+          default:
+            throw new StorageException(
+                String.format("Delete star ref %s failed: %s", refName, result.name()));
+        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index d6ea294..1f2bc8c 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.ACCOUNTS_UPDATE;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
@@ -45,6 +46,7 @@
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryableAction.Action;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -443,28 +445,32 @@
 
   private ImmutableList<Optional<AccountState>> execute(List<ExecutableUpdate> executableUpdates)
       throws IOException, ConfigInvalidException {
-    List<Optional<AccountState>> accountState = new ArrayList<>();
-    List<UpdatedAccount> updatedAccounts = new ArrayList<>();
-    executeWithRetry(
-        () -> {
-          // Reset state for retry.
-          externalIdNotes = null;
-          accountState.clear();
-          updatedAccounts.clear();
+    try (RefUpdateContext ctx = RefUpdateContext.open(ACCOUNTS_UPDATE)) {
+      List<Optional<AccountState>> accountState = new ArrayList<>();
+      List<UpdatedAccount> updatedAccounts = new ArrayList<>();
+      executeWithRetry(
+          () -> {
 
-          try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-            for (ExecutableUpdate executableUpdate : executableUpdates) {
-              updatedAccounts.add(executableUpdate.execute(allUsersRepo));
+            // Reset state for retry.
+            externalIdNotes = null;
+            accountState.clear();
+            updatedAccounts.clear();
+            try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+              for (ExecutableUpdate executableUpdate : executableUpdates) {
+                updatedAccounts.add(executableUpdate.execute(allUsersRepo));
+              }
+              commit(
+                  allUsersRepo,
+                  updatedAccounts.stream().filter(Objects::nonNull).collect(toList()));
+              for (UpdatedAccount ua : updatedAccounts) {
+                accountState.add(ua == null ? Optional.empty() : ua.getAccountState());
+              }
             }
-            commit(
-                allUsersRepo, updatedAccounts.stream().filter(Objects::nonNull).collect(toList()));
-            for (UpdatedAccount ua : updatedAccounts) {
-              accountState.add(ua == null ? Optional.empty() : ua.getAccountState());
-            }
-          }
-          return null;
-        });
-    return ImmutableList.copyOf(accountState);
+            return null;
+          });
+
+      return ImmutableList.copyOf(accountState);
+    }
   }
 
   private void executeWithRetry(Action<Void> action) throws IOException, ConfigInvalidException {
diff --git a/java/com/google/gerrit/server/account/externalids/testing/BUILD b/java/com/google/gerrit/server/account/externalids/testing/BUILD
index 0e469e3..e2de6da 100644
--- a/java/com/google/gerrit/server/account/externalids/testing/BUILD
+++ b/java/com/google/gerrit/server/account/externalids/testing/BUILD
@@ -8,6 +8,7 @@
     deps = [
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//lib:jgit",
     ],
 )
diff --git a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
index a42afc3..7878ee2 100644
--- a/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
+++ b/java/com/google/gerrit/server/account/externalids/testing/ExternalIdTestUtil.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account.externalids.testing;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
@@ -143,7 +144,7 @@
       RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS);
       u.setExpectedOldObjectId(rev);
       u.setNewObjectId(commitId);
-      RefUpdate.Result res = u.update();
+      RefUpdate.Result res = testRefAction(() -> u.update());
       switch (res) {
         case NEW:
         case FAST_FORWARD:
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 3a892bc..ad42ae6 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 import static com.google.gerrit.server.restapi.project.DashboardsCollection.DEFAULT_DASHBOARD_NAME;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.HEAD_MODIFICATION;
 import static java.util.stream.Collectors.toList;
 
 import com.google.gerrit.extensions.api.access.ProjectAccessInfo;
@@ -89,6 +91,7 @@
 import com.google.gerrit.server.restapi.project.SetAccess;
 import com.google.gerrit.server.restapi.project.SetHead;
 import com.google.gerrit.server.restapi.project.SetParent;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -595,7 +598,9 @@
   @Override
   public void deleteBranches(DeleteBranchesInput in) throws RestApiException {
     try {
-      deleteBranches.apply(checkExists(), in);
+      try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
+        deleteBranches.apply(checkExists(), in);
+      }
     } catch (Exception e) {
       throw asRestApiException("Cannot delete branches", e);
     }
@@ -686,7 +691,9 @@
     HeadInput input = new HeadInput();
     input.ref = head;
     try {
-      setHead.apply(checkExists(), input);
+      try (RefUpdateContext ctx = RefUpdateContext.open(HEAD_MODIFICATION)) {
+        setHead.apply(checkExists(), input);
+      }
     } catch (Exception e) {
       throw asRestApiException("Cannot set HEAD", e);
     }
diff --git a/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/java/com/google/gerrit/server/api/projects/TagApiImpl.java
index 005486a..f9bd048 100644
--- a/java/com/google/gerrit/server/api/projects/TagApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.api.projects;
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.TAG_MODIFICATION;
 
 import com.google.gerrit.extensions.api.projects.TagApi;
 import com.google.gerrit.extensions.api.projects.TagInfo;
@@ -29,6 +30,7 @@
 import com.google.gerrit.server.restapi.project.DeleteTag;
 import com.google.gerrit.server.restapi.project.ListTags;
 import com.google.gerrit.server.restapi.project.TagsCollection;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
@@ -83,7 +85,9 @@
   @Override
   public void delete() throws RestApiException {
     try {
-      deleteTag.apply(resource(), new Input());
+      try (RefUpdateContext ctx = RefUpdateContext.open(TAG_MODIFICATION)) {
+        deleteTag.apply(resource(), new Input());
+      }
     } catch (Exception e) {
       throw asRestApiException("Cannot delete tag", e);
     }
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index 2efa027..9070006 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -25,6 +27,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -68,24 +71,27 @@
       return;
     }
     AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
-    try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.now())) {
-      u.setNotify(notify);
-      for (ChangeData change : changes) {
-        if (!project.equals(change.project())) {
-          throw new ResourceConflictException(
-              String.format(
-                  "Project name \"%s\" doesn't match \"%s\"",
-                  change.project().get(), project.get()));
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.now())) {
+        u.setNotify(notify);
+        for (ChangeData change : changes) {
+          if (!project.equals(change.project())) {
+            throw new ResourceConflictException(
+                String.format(
+                    "Project name \"%s\" doesn't match \"%s\"",
+                    change.project().get(), project.get()));
+          }
+          u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt));
+          u.addOp(
+              change.getId(),
+              storeSubmitRequirementsOpFactory.create(
+                  change.submitRequirements().values(), change));
         }
-        u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt));
-        u.addOp(
-            change.getId(),
-            storeSubmitRequirementsOpFactory.create(change.submitRequirements().values(), change));
-      }
-      u.execute();
+        u.execute();
 
-      if (cfg.getCleanupAccountPatchReview()) {
-        cleanupAccountPatchReview(changes);
+        if (cfg.getCleanupAccountPatchReview()) {
+          cleanupAccountPatchReview(changes);
+        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 38efc44..063903b 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -18,6 +18,7 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
 import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
 
@@ -57,6 +58,7 @@
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -174,29 +176,31 @@
   public Result check(ChangeNotes notes, @Nullable FixInput f) {
     requireNonNull(notes);
     try {
-      return retryHelper
-          .changeUpdate(
-              "checkChangeConsistency",
-              buf -> {
-                try {
-                  reset();
-                  this.updateFactory = buf;
-                  this.notes = notes;
-                  fix = f;
-                  checkImpl();
-                  return result();
-                } finally {
-                  if (rw != null) {
-                    rw.getObjectReader().close();
-                    rw.close();
-                    oi.close();
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        return retryHelper
+            .changeUpdate(
+                "checkChangeConsistency",
+                buf -> {
+                  try {
+                    reset();
+                    this.updateFactory = buf;
+                    this.notes = notes;
+                    fix = f;
+                    checkImpl();
+                    return result();
+                  } finally {
+                    if (rw != null) {
+                      rw.getObjectReader().close();
+                      rw.close();
+                      oi.close();
+                    }
+                    if (repo != null) {
+                      repo.close();
+                    }
                   }
-                  if (repo != null) {
-                    repo.close();
-                  }
-                }
-              })
-          .call();
+                })
+            .call();
+      }
     } catch (RestApiException e) {
       return logAndReturnOneProblem(e, notes, "Error checking change: " + e.getMessage());
     } catch (UpdateException e) {
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index ad0dd8b..a2ce866 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.edit;
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.base.Charsets;
 import com.google.gerrit.common.Nullable;
@@ -49,6 +50,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -408,8 +410,10 @@
     ObjectId newEditCommit =
         createCommit(repository, basePatchsetCommit, newTreeId, newCommitMessage, nowTimestamp);
 
-    return editBehavior.updateEditInStorage(
-        repository, notes, basePatchset, newEditCommit, nowTimestamp);
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      return editBehavior.updateEditInStorage(
+          repository, notes, basePatchset, newEditCommit, nowTimestamp);
+    }
   }
 
   private void assertCanEdit(ChangeNotes notes)
@@ -801,23 +805,25 @@
         ObjectId targetObjectId,
         Instant timestamp)
         throws IOException {
-      RefUpdate ru = repository.updateRef(refName);
-      ru.setExpectedOldObjectId(currentObjectId);
-      ru.setNewObjectId(targetObjectId);
-      ru.setRefLogIdent(getRefLogIdent(timestamp));
-      ru.setRefLogMessage("inline edit (amend)", false);
-      ru.setForceUpdate(true);
-      try (RevWalk revWalk = new RevWalk(repository)) {
-        RefUpdate.Result res = ru.update(revWalk);
-        String message = "cannot update " + ru.getName() + " in " + projectName + ": " + res;
-        if (res == RefUpdate.Result.LOCK_FAILURE) {
-          throw new LockFailureException(message, ru);
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        RefUpdate ru = repository.updateRef(refName);
+        ru.setExpectedOldObjectId(currentObjectId);
+        ru.setNewObjectId(targetObjectId);
+        ru.setRefLogIdent(getRefLogIdent(timestamp));
+        ru.setRefLogMessage("inline edit (amend)", false);
+        ru.setForceUpdate(true);
+        try (RevWalk revWalk = new RevWalk(repository)) {
+          RefUpdate.Result res = ru.update(revWalk);
+          String message = "cannot update " + ru.getName() + " in " + projectName + ": " + res;
+          if (res == RefUpdate.Result.LOCK_FAILURE) {
+            throw new LockFailureException(message, ru);
+          }
+          if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) {
+            throw new IOException(message);
+          }
         }
-        if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) {
-          throw new IOException(message);
-        }
+        gitReferenceUpdated.fire(projectName, ru, getUpdater());
       }
-      gitReferenceUpdated.fire(projectName, ru, getUpdater());
     }
 
     void baseEditOnDifferentPatchset(
@@ -850,21 +856,24 @@
         ObjectId targetObjectId,
         Instant timestamp)
         throws IOException {
-      BatchRefUpdate batchRefUpdate = repository.getRefDatabase().newBatchUpdate();
-      batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(), targetObjectId, newRefName));
-      batchRefUpdate.addCommand(
-          new ReceiveCommand(currentObjectId, ObjectId.zeroId(), currentRefName));
-      batchRefUpdate.setRefLogMessage("rebase edit", false);
-      batchRefUpdate.setRefLogIdent(getRefLogIdent(timestamp));
-      try (RevWalk revWalk = new RevWalk(repository)) {
-        batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
-      }
-      for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
-        if (cmd.getResult() != ReceiveCommand.Result.OK) {
-          throw new IOException("failed: " + cmd);
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        BatchRefUpdate batchRefUpdate = repository.getRefDatabase().newBatchUpdate();
+        batchRefUpdate.addCommand(
+            new ReceiveCommand(ObjectId.zeroId(), targetObjectId, newRefName));
+        batchRefUpdate.addCommand(
+            new ReceiveCommand(currentObjectId, ObjectId.zeroId(), currentRefName));
+        batchRefUpdate.setRefLogMessage("rebase edit", false);
+        batchRefUpdate.setRefLogIdent(getRefLogIdent(timestamp));
+        try (RevWalk revWalk = new RevWalk(repository)) {
+          batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
         }
+        for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
+          if (cmd.getResult() != ReceiveCommand.Result.OK) {
+            throw new IOException("failed: " + cmd);
+          }
+        }
+        gitReferenceUpdated.fire(projectName, batchRefUpdate, getUpdater());
       }
-      gitReferenceUpdated.fire(projectName, batchRefUpdate, getUpdater());
     }
 
     static RevCommit lookupCommit(Repository repository, ObjectId commitId) throws IOException {
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 3474590..3594afb 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.edit;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.RepoContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -189,20 +191,22 @@
       } else {
         message.append("Published edit on patch set ").append(basePatchSet.number()).append(".");
       }
-
-      try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.now())) {
-        bu.setRepository(repo, rw, oi);
-        bu.setNotify(notify);
-        bu.addOp(change.getId(), inserter.setMessage(message.toString()));
-        bu.addOp(
-            change.getId(),
-            new BatchUpdateOp() {
-              @Override
-              public void updateRepo(RepoContext ctx) throws Exception {
-                ctx.addRefUpdate(edit.getEditCommit().copy(), ObjectId.zeroId(), edit.getRefName());
-              }
-            });
-        bu.execute();
+      try (RefUpdateContext changeCtx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.now())) {
+          bu.setRepository(repo, rw, oi);
+          bu.setNotify(notify);
+          bu.addOp(change.getId(), inserter.setMessage(message.toString()));
+          bu.addOp(
+              change.getId(),
+              new BatchUpdateOp() {
+                @Override
+                public void updateRepo(RepoContext ctx) throws Exception {
+                  ctx.addRefUpdate(
+                      edit.getEditCommit().copy(), ObjectId.zeroId(), edit.getRefName());
+                }
+              });
+          bu.execute();
+        }
       }
     }
   }
@@ -243,33 +247,35 @@
   }
 
   private void deleteRef(Repository repo, ChangeEdit edit) throws IOException {
-    String refName = edit.getRefName();
-    RefUpdate ru = repo.updateRef(refName, true);
-    ru.setExpectedOldObjectId(edit.getEditCommit());
-    ru.setForceUpdate(true);
-    RefUpdate.Result result = ru.delete();
-    switch (result) {
-      case FORCED:
-      case NEW:
-      case NO_CHANGE:
-        break;
-      case LOCK_FAILURE:
-        throw new LockFailureException(String.format("Failed to delete ref %s", refName), ru);
-      case FAST_FORWARD:
-      case IO_FAILURE:
-      case NOT_ATTEMPTED:
-      case REJECTED:
-      case REJECTED_CURRENT_BRANCH:
-      case RENAMED:
-      case REJECTED_MISSING_OBJECT:
-      case REJECTED_OTHER_REASON:
-      default:
-        throw new IOException(String.format("Failed to delete ref %s: %s", refName, result));
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      String refName = edit.getRefName();
+      RefUpdate ru = repo.updateRef(refName, true);
+      ru.setExpectedOldObjectId(edit.getEditCommit());
+      ru.setForceUpdate(true);
+      RefUpdate.Result result = ru.delete();
+      switch (result) {
+        case FORCED:
+        case NEW:
+        case NO_CHANGE:
+          break;
+        case LOCK_FAILURE:
+          throw new LockFailureException(String.format("Failed to delete ref %s", refName), ru);
+        case FAST_FORWARD:
+        case IO_FAILURE:
+        case NOT_ATTEMPTED:
+        case REJECTED:
+        case REJECTED_CURRENT_BRANCH:
+        case RENAMED:
+        case REJECTED_MISSING_OBJECT:
+        case REJECTED_OTHER_REASON:
+        default:
+          throw new IOException(String.format("Failed to delete ref %s: %s", refName, result));
+      }
+      gitReferenceUpdated.fire(
+          edit.getChange().getProject(),
+          ru,
+          /* updater= */ userProvider.get().asIdentifiedUser().state());
     }
-    gitReferenceUpdated.fire(
-        edit.getChange().getProject(),
-        ru,
-        /* updater= */ userProvider.get().asIdentifiedUser().state());
   }
 
   private static RevCommit writeSquashedCommit(
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index e27197c..e00012a 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import static com.google.gerrit.entities.RefNames.REFS_REJECT_COMMITS;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BAN_COMMIT;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.entities.Project;
@@ -26,6 +27,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -128,21 +130,23 @@
         banCommitNotes.set(commitToBan, noteId);
       }
       NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(project, repo, inserter);
-      NoteMap newlyCreated =
-          notesBranchUtil.commitNewNotes(
-              banCommitNotes,
-              REFS_REJECT_COMMITS,
-              createPersonIdent(),
-              buildCommitMessage(commitsToBan, reason));
+      try (RefUpdateContext ctx = RefUpdateContext.open(BAN_COMMIT)) {
+        NoteMap newlyCreated =
+            notesBranchUtil.commitNewNotes(
+                banCommitNotes,
+                REFS_REJECT_COMMITS,
+                createPersonIdent(),
+                buildCommitMessage(commitsToBan, reason));
 
-      for (Note n : banCommitNotes) {
-        if (newlyCreated.contains(n)) {
-          result.commitBanned(n);
-        } else {
-          result.commitAlreadyBanned(n);
+        for (Note n : banCommitNotes) {
+          if (newlyCreated.contains(n)) {
+            result.commitBanned(n);
+          } else {
+            result.commitAlreadyBanned(n);
+          }
         }
+        return result;
       }
-      return result;
     }
   }
 
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index f0b2a78..ffb6c66 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
@@ -54,6 +55,7 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.PostUpdateContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -162,18 +164,19 @@
       ChangeNotes notes, CurrentUser user, RevertInput input, Instant timestamp)
       throws RestApiException, UpdateException, ConfigInvalidException, IOException {
     String message = Strings.emptyToNull(input.message);
-
-    try (Repository git = repoManager.openRepository(notes.getProjectName());
-        ObjectInserter oi = git.newObjectInserter();
-        ObjectReader reader = oi.newReader();
-        RevWalk revWalk = new RevWalk(reader)) {
-      ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
-      ObjectId revCommit =
-          createRevertCommit(message, notes, user, timestamp, oi, revWalk, generatedChangeId);
-      return createRevertChangeFromCommit(
-          revCommit, input, notes, user, generatedChangeId, timestamp, oi, revWalk, git);
-    } catch (RepositoryNotFoundException e) {
-      throw new ResourceNotFoundException(notes.getChangeId().toString(), e);
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (Repository git = repoManager.openRepository(notes.getProjectName());
+          ObjectInserter oi = git.newObjectInserter();
+          ObjectReader reader = oi.newReader();
+          RevWalk revWalk = new RevWalk(reader)) {
+        ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
+        ObjectId revCommit =
+            createRevertCommit(message, notes, user, timestamp, oi, revWalk, generatedChangeId);
+        return createRevertChangeFromCommit(
+            revCommit, input, notes, user, generatedChangeId, timestamp, oi, revWalk, git);
+      } catch (RepositoryNotFoundException e) {
+        throw new ResourceNotFoundException(notes.getChangeId().toString(), e);
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index 61bd8a8..4f0bde8 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git.meta;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.VERSIONED_META_DATA_CHANGE;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.flogger.FluentLogger;
@@ -27,6 +28,7 @@
 import com.google.gerrit.server.logging.Metadata;
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import java.io.BufferedReader;
 import java.io.File;
@@ -438,53 +440,55 @@
 
       private RevCommit updateRef(AnyObjectId oldId, AnyObjectId newId, String refName)
           throws IOException {
-        BatchRefUpdate bru = update.getBatch();
-        if (bru != null) {
-          bru.addCommand(new ReceiveCommand(oldId.toObjectId(), newId.toObjectId(), refName));
-          if (objInserter == null) {
-            inserter.flush();
-          }
-          revision = rw.parseCommit(newId);
-          return revision;
-        }
-
-        RefUpdate ru = db.updateRef(refName);
-        ru.setExpectedOldObjectId(oldId);
-        ru.setNewObjectId(newId);
-        ru.setRefLogIdent(update.getCommitBuilder().getAuthor());
-        String message = update.getCommitBuilder().getMessage();
-        if (message == null) {
-          message = "meta data update";
-        }
-        try (BufferedReader reader = new BufferedReader(new StringReader(message))) {
-          // read the subject line and use it as reflog message
-          ru.setRefLogMessage("commit: " + reader.readLine(), true);
-        }
-        logger.atFine().log("Saving commit '%s' on project '%s'", message.trim(), projectName);
-        inserter.flush();
-        RefUpdate.Result result = ru.update();
-        switch (result) {
-          case NEW:
-          case FAST_FORWARD:
-            revision = rw.parseCommit(ru.getNewObjectId());
-            update.fireGitRefUpdatedEvent(ru);
-            logger.atFine().log(
-                "Saved commit '%s' as revision '%s' on project '%s'",
-                message.trim(), revision.name(), projectName);
+        try (RefUpdateContext ctx = RefUpdateContext.open(VERSIONED_META_DATA_CHANGE)) {
+          BatchRefUpdate bru = update.getBatch();
+          if (bru != null) {
+            bru.addCommand(new ReceiveCommand(oldId.toObjectId(), newId.toObjectId(), refName));
+            if (objInserter == null) {
+              inserter.flush();
+            }
+            revision = rw.parseCommit(newId);
             return revision;
-          case LOCK_FAILURE:
-            throw new LockFailureException(errorMsg(ru, db.getDirectory()), ru);
-          case FORCED:
-          case IO_FAILURE:
-          case NOT_ATTEMPTED:
-          case NO_CHANGE:
-          case REJECTED:
-          case REJECTED_CURRENT_BRANCH:
-          case RENAMED:
-          case REJECTED_MISSING_OBJECT:
-          case REJECTED_OTHER_REASON:
-          default:
-            throw new GitUpdateFailureException(errorMsg(ru, db.getDirectory()), ru);
+          }
+
+          RefUpdate ru = db.updateRef(refName);
+          ru.setExpectedOldObjectId(oldId);
+          ru.setNewObjectId(newId);
+          ru.setRefLogIdent(update.getCommitBuilder().getAuthor());
+          String message = update.getCommitBuilder().getMessage();
+          if (message == null) {
+            message = "meta data update";
+          }
+          try (BufferedReader reader = new BufferedReader(new StringReader(message))) {
+            // read the subject line and use it as reflog message
+            ru.setRefLogMessage("commit: " + reader.readLine(), true);
+          }
+          logger.atFine().log("Saving commit '%s' on project '%s'", message.trim(), projectName);
+          inserter.flush();
+          RefUpdate.Result result = ru.update();
+          switch (result) {
+            case NEW:
+            case FAST_FORWARD:
+              revision = rw.parseCommit(ru.getNewObjectId());
+              update.fireGitRefUpdatedEvent(ru);
+              logger.atFine().log(
+                  "Saved commit '%s' as revision '%s' on project '%s'",
+                  message.trim(), revision.name(), projectName);
+              return revision;
+            case LOCK_FAILURE:
+              throw new LockFailureException(errorMsg(ru, db.getDirectory()), ru);
+            case FORCED:
+            case IO_FAILURE:
+            case NOT_ATTEMPTED:
+            case NO_CHANGE:
+            case REJECTED:
+            case REJECTED_CURRENT_BRANCH:
+            case RENAMED:
+            case REJECTED_MISSING_OBJECT:
+            case REJECTED_OTHER_REASON:
+            default:
+              throw new GitUpdateFailureException(errorMsg(ru, db.getDirectory()), ru);
+          }
         }
       }
     };
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 093ca78..d25e022 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -33,6 +33,8 @@
 import static com.google.gerrit.server.git.validators.CommitValidators.NEW_PATCHSET_PATTERN;
 import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.DIRECT_PUSH;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.joining;
@@ -192,6 +194,7 @@
 import com.google.gerrit.server.update.SubmissionListener;
 import com.google.gerrit.server.update.SuperprojectUpdateOnSubmission;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.RequestScopePropagator;
@@ -759,13 +762,15 @@
         String pushKind = magicBranch != null && magicBranch.submit ? "direct_submit" : "magic";
         metrics.pushCount.increment(pushKind, project.getName(), getUpdateType(magicCommands));
       }
-      if (!regularCommands.isEmpty()) {
-        metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
-      }
+      try (RefUpdateContext ctx = RefUpdateContext.open(DIRECT_PUSH)) {
+        if (!regularCommands.isEmpty()) {
+          metrics.pushCount.increment("direct", project.getName(), getUpdateType(regularCommands));
+        }
 
-      if (!regularCommands.isEmpty()) {
-        handleRegularCommands(regularCommands, progress);
-        return;
+        if (!regularCommands.isEmpty()) {
+          handleRegularCommands(regularCommands, progress);
+          return;
+        }
       }
 
       boolean first = true;
@@ -1015,120 +1020,124 @@
 
   private void insertChangesAndPatchSets(
       ImmutableList<CreateRequest> newChanges, Task replaceProgress) {
-    try (TraceTimer traceTimer =
-        newTimer(
-            "insertChangesAndPatchSets", Metadata.builder().resourceCount(newChanges.size()))) {
-      ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
-      if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
-        logger.atWarning().log(
-            "Skipping change updates on %s because ref update failed: %s %s",
-            project.getName(),
-            magicBranchCmd.getResult(),
-            Strings.nullToEmpty(magicBranchCmd.getMessage()));
-        return;
-      }
-
-      try (BatchUpdate bu =
-              batchUpdateFactory.create(
-                  project.getNameKey(), user.materializedCopy(), TimeUtil.now());
-          ObjectInserter ins = repo.newObjectInserter();
-          ObjectReader reader = ins.newReader();
-          RevWalk rw = new RevWalk(reader)) {
-        bu.setRepository(repo, rw, ins);
-        bu.setRefLogMessage("push");
-        if (magicBranch != null) {
-          bu.setNotify(magicBranch.getNotifyForNewChange());
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (TraceTimer traceTimer =
+          newTimer(
+              "insertChangesAndPatchSets", Metadata.builder().resourceCount(newChanges.size()))) {
+        ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null;
+        if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) {
+          logger.atWarning().log(
+              "Skipping change updates on %s because ref update failed: %s %s",
+              project.getName(),
+              magicBranchCmd.getResult(),
+              Strings.nullToEmpty(magicBranchCmd.getMessage()));
+          return;
         }
 
-        logger.atFine().log("Adding %d replace requests", newChanges.size());
-        for (ReplaceRequest replace : replaceByChange.values()) {
-          replace.addOps(bu, replaceProgress);
+        try (BatchUpdate bu =
+                batchUpdateFactory.create(
+                    project.getNameKey(), user.materializedCopy(), TimeUtil.now());
+            ObjectInserter ins = repo.newObjectInserter();
+            ObjectReader reader = ins.newReader();
+            RevWalk rw = new RevWalk(reader)) {
+          bu.setRepository(repo, rw, ins);
+          bu.setRefLogMessage("push");
           if (magicBranch != null) {
-            bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
-            if (magicBranch.shouldPublishComments()) {
-              bu.addOp(
-                  replace.notes.getChangeId(),
-                  publishCommentsOp.create(replace.psId, project.getNameKey()));
-              Optional<ChangeNotes> changeNotes = getChangeNotes(replace.notes.getChangeId());
-              if (!changeNotes.isPresent()) {
-                // If not present, no need to update attention set here since this is a new change.
-                continue;
+            bu.setNotify(magicBranch.getNotifyForNewChange());
+          }
+
+          logger.atFine().log("Adding %d replace requests", newChanges.size());
+          for (ReplaceRequest replace : replaceByChange.values()) {
+            replace.addOps(bu, replaceProgress);
+            if (magicBranch != null) {
+              bu.setNotifyHandling(
+                  replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
+              if (magicBranch.shouldPublishComments()) {
+                bu.addOp(
+                    replace.notes.getChangeId(),
+                    publishCommentsOp.create(replace.psId, project.getNameKey()));
+                Optional<ChangeNotes> changeNotes = getChangeNotes(replace.notes.getChangeId());
+                if (!changeNotes.isPresent()) {
+                  // If not present, no need to update attention set here since this is a new
+                  // change.
+                  continue;
+                }
+                List<HumanComment> drafts =
+                    commentsUtil.draftByChangeAuthor(changeNotes.get(), user.getAccountId());
+                if (drafts.isEmpty()) {
+                  // If no comments, attention set shouldn't update since the user didn't reply.
+                  continue;
+                }
+                replyAttentionSetUpdates.processAutomaticAttentionSetRulesOnReply(
+                    bu, changeNotes.get(), isReadyForReview(changeNotes.get()), user, drafts);
               }
-              List<HumanComment> drafts =
-                  commentsUtil.draftByChangeAuthor(changeNotes.get(), user.getAccountId());
-              if (drafts.isEmpty()) {
-                // If no comments, attention set shouldn't update since the user didn't reply.
-                continue;
-              }
-              replyAttentionSetUpdates.processAutomaticAttentionSetRulesOnReply(
-                  bu, changeNotes.get(), isReadyForReview(changeNotes.get()), user, drafts);
             }
           }
-        }
 
-        logger.atFine().log("Adding %d create requests", newChanges.size());
-        for (CreateRequest create : newChanges) {
-          create.addOps(bu);
-        }
-
-        logger.atFine().log("Adding %d group update requests", newChanges.size());
-        updateGroups.forEach(r -> r.addOps(bu));
-
-        logger.atFine().log("Executing batch");
-        try {
-          bu.execute();
-        } catch (UpdateException e) {
-          throw asRestApiException(e);
-        }
-
-        replaceByChange.values().stream()
-            .forEach(
-                req ->
-                    result.addChange(ReceiveCommitsResult.ChangeStatus.REPLACED, req.ontoChange));
-        newChanges.stream()
-            .forEach(
-                req -> result.addChange(ReceiveCommitsResult.ChangeStatus.CREATED, req.changeId));
-
-        if (magicBranchCmd != null) {
-          magicBranchCmd.setResult(OK);
-        }
-        for (ReplaceRequest replace : replaceByChange.values()) {
-          String rejectMessage = replace.getRejectMessage();
-          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 {
-            logger.atFine().log("Rejecting due to message from ReplaceOp");
-            reject(replace.inputCommand, rejectMessage);
+          logger.atFine().log("Adding %d create requests", newChanges.size());
+          for (CreateRequest create : newChanges) {
+            create.addOps(bu);
           }
-        }
 
-      } catch (ResourceConflictException e) {
-        addError(e.getMessage());
-        reject(magicBranchCmd, "conflict");
-      } catch (BadRequestException | UnprocessableEntityException | AuthException e) {
-        logger.atFine().withCause(e).log("Rejecting due to client error");
-        reject(magicBranchCmd, e.getMessage());
-      } catch (RestApiException | IOException e) {
-        throw new StorageException("Can't insert change/patch set for " + project.getName(), e);
-      }
+          logger.atFine().log("Adding %d group update requests", newChanges.size());
+          updateGroups.forEach(r -> r.addOps(bu));
 
-      if (magicBranch != null && magicBranch.submit) {
-        try {
-          submit(newChanges, replaceByChange.values());
+          logger.atFine().log("Executing batch");
+          try {
+            bu.execute();
+          } catch (UpdateException e) {
+            throw asRestApiException(e);
+          }
+
+          replaceByChange.values().stream()
+              .forEach(
+                  req ->
+                      result.addChange(ReceiveCommitsResult.ChangeStatus.REPLACED, req.ontoChange));
+          newChanges.stream()
+              .forEach(
+                  req -> result.addChange(ReceiveCommitsResult.ChangeStatus.CREATED, req.changeId));
+
+          if (magicBranchCmd != null) {
+            magicBranchCmd.setResult(OK);
+          }
+          for (ReplaceRequest replace : replaceByChange.values()) {
+            String rejectMessage = replace.getRejectMessage();
+            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 {
+              logger.atFine().log("Rejecting due to message from ReplaceOp");
+              reject(replace.inputCommand, rejectMessage);
+            }
+          }
+
         } catch (ResourceConflictException e) {
           addError(e.getMessage());
           reject(magicBranchCmd, "conflict");
-        } catch (RestApiException
-            | StorageException
-            | UpdateException
-            | IOException
-            | ConfigInvalidException
-            | PermissionBackendException e) {
-          logger.atSevere().withCause(e).log("Error submitting changes to %s", project.getName());
-          reject(magicBranchCmd, "error during submit");
+        } catch (BadRequestException | UnprocessableEntityException | AuthException e) {
+          logger.atFine().withCause(e).log("Rejecting due to client error");
+          reject(magicBranchCmd, e.getMessage());
+        } catch (RestApiException | IOException e) {
+          throw new StorageException("Can't insert change/patch set for " + project.getName(), e);
+        }
+
+        if (magicBranch != null && magicBranch.submit) {
+          try {
+            submit(newChanges, replaceByChange.values());
+          } catch (ResourceConflictException e) {
+            addError(e.getMessage());
+            reject(magicBranchCmd, "conflict");
+          } catch (RestApiException
+              | StorageException
+              | UpdateException
+              | IOException
+              | ConfigInvalidException
+              | PermissionBackendException e) {
+            logger.atSevere().withCause(e).log("Error submitting changes to %s", project.getName());
+            reject(magicBranchCmd, "error during submit");
+          }
         }
       }
     }
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 87d8db1..14f8825 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.group.db;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GROUPS_UPDATE;
+
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
@@ -45,6 +47,7 @@
 import com.google.gerrit.server.logging.TraceContext;
 import com.google.gerrit.server.logging.TraceContext.TraceTimer;
 import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -305,16 +308,18 @@
   private InternalGroup createGroupInNoteDbWithRetry(
       InternalGroupCreation groupCreation, GroupDelta groupDelta)
       throws IOException, ConfigInvalidException, DuplicateKeyException {
-    try {
-      return retryHelper
-          .groupUpdate("createGroup", () -> createGroupInNoteDb(groupCreation, groupDelta))
-          .call();
-    } catch (Exception e) {
-      Throwables.throwIfUnchecked(e);
-      Throwables.throwIfInstanceOf(e, IOException.class);
-      Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
-      Throwables.throwIfInstanceOf(e, DuplicateKeyException.class);
-      throw new IOException(e);
+    try (RefUpdateContext ctx = RefUpdateContext.open(GROUPS_UPDATE)) {
+      try {
+        return retryHelper
+            .groupUpdate("createGroup", () -> createGroupInNoteDb(groupCreation, groupDelta))
+            .call();
+      } catch (Exception e) {
+        Throwables.throwIfUnchecked(e);
+        Throwables.throwIfInstanceOf(e, IOException.class);
+        Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
+        Throwables.throwIfInstanceOf(e, DuplicateKeyException.class);
+        throw new IOException(e);
+      }
     }
   }
 
@@ -361,30 +366,32 @@
   @VisibleForTesting
   public UpdateResult updateGroupInNoteDb(AccountGroup.UUID groupUuid, GroupDelta groupDelta)
       throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException {
-    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-      GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, groupUuid);
-      groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
-      if (!groupConfig.getLoadedGroup().isPresent()) {
-        throw new NoSuchGroupException(groupUuid);
+    try (RefUpdateContext ctx = RefUpdateContext.open(GROUPS_UPDATE)) {
+      try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+        GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, groupUuid);
+        groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
+        if (!groupConfig.getLoadedGroup().isPresent()) {
+          throw new NoSuchGroupException(groupUuid);
+        }
+
+        InternalGroup originalGroup = groupConfig.getLoadedGroup().get();
+        GroupNameNotes groupNameNotes = null;
+        if (groupDelta.getName().isPresent()) {
+          AccountGroup.NameKey oldName = originalGroup.getNameKey();
+          AccountGroup.NameKey newName = groupDelta.getName().get();
+          groupNameNotes =
+              GroupNameNotes.forRename(allUsersName, allUsersRepo, groupUuid, oldName, newName);
+        }
+
+        commit(allUsersRepo, groupConfig, groupNameNotes);
+
+        InternalGroup updatedGroup =
+            groupConfig
+                .getLoadedGroup()
+                .orElseThrow(
+                    () -> new IllegalStateException("Updated group wasn't automatically loaded"));
+        return getUpdateResult(originalGroup, updatedGroup);
       }
-
-      InternalGroup originalGroup = groupConfig.getLoadedGroup().get();
-      GroupNameNotes groupNameNotes = null;
-      if (groupDelta.getName().isPresent()) {
-        AccountGroup.NameKey oldName = originalGroup.getNameKey();
-        AccountGroup.NameKey newName = groupDelta.getName().get();
-        groupNameNotes =
-            GroupNameNotes.forRename(allUsersName, allUsersRepo, groupUuid, oldName, newName);
-      }
-
-      commit(allUsersRepo, groupConfig, groupNameNotes);
-
-      InternalGroup updatedGroup =
-          groupConfig
-              .getLoadedGroup()
-              .orElseThrow(
-                  () -> new IllegalStateException("Updated group wasn't automatically loaded"));
-      return getUpdateResult(originalGroup, updatedGroup);
     }
   }
 
diff --git a/java/com/google/gerrit/server/group/db/testing/BUILD b/java/com/google/gerrit/server/group/db/testing/BUILD
index 8f33f98..a4f49e9 100644
--- a/java/com/google/gerrit/server/group/db/testing/BUILD
+++ b/java/com/google/gerrit/server/group/db/testing/BUILD
@@ -8,6 +8,7 @@
     srcs = glob(["*.java"]),
     deps = [
         "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//lib:guava",
         "//lib:jgit",
         "//lib:jgit-junit",
diff --git a/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
index fa06281..e36ccf0 100644
--- a/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
+++ b/java/com/google/gerrit/server/group/db/testing/GroupTestUtil.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.group.db.testing;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
+
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
@@ -45,25 +48,27 @@
       String fileName,
       String contents)
       throws Exception {
-    try (RevWalk rw = new RevWalk(allUsersRepo);
-        TestRepository<Repository> testRepository = new TestRepository<>(allUsersRepo, rw)) {
-      TestRepository<Repository>.CommitBuilder builder =
-          testRepository
-              .branch(refName)
-              .commit()
-              .add(fileName, contents)
-              .message("update group file")
-              .author(serverIdent)
-              .committer(serverIdent);
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (RevWalk rw = new RevWalk(allUsersRepo);
+          TestRepository<Repository> testRepository = new TestRepository<>(allUsersRepo, rw)) {
+        TestRepository<Repository>.CommitBuilder builder =
+            testRepository
+                .branch(refName)
+                .commit()
+                .add(fileName, contents)
+                .message("update group file")
+                .author(serverIdent)
+                .committer(serverIdent);
 
-      Ref ref = allUsersRepo.exactRef(refName);
-      if (ref != null) {
-        RevCommit c = rw.parseCommit(ref.getObjectId());
-        if (c != null) {
-          builder.parent(c);
+        Ref ref = allUsersRepo.exactRef(refName);
+        if (ref != null) {
+          RevCommit c = rw.parseCommit(ref.getObjectId());
+          if (c != null) {
+            builder.parent(c);
+          }
         }
+        builder.create();
       }
-      builder.create();
     }
   }
 
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 8e443f82..7984737 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -505,9 +505,11 @@
 
   /** The user assigned to the change. */
   // The getter always returns NO_ASSIGNEE, since assignee field is deprecated.
+  @Deprecated
   public static final IndexedField<ChangeData, Integer> ASSIGNEE_FIELD =
       IndexedField.<ChangeData>integerBuilder("Assignee").build(changeGetter(c -> NO_ASSIGNEE));
 
+  @Deprecated
   public static final IndexedField<ChangeData, Integer>.SearchSpec ASSIGNEE_SPEC =
       ASSIGNEE_FIELD.integer(ChangeQueryBuilder.FIELD_ASSIGNEE);
 
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 6ddf7a3..82b8f18 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -240,6 +240,7 @@
           .build();
 
   /** Remove assignee field. */
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V82 =
       new Schema.Builder<ChangeData>()
           .add(V81)
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 2b8a501..93da997 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail.receive;
 
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Strings;
@@ -69,6 +70,7 @@
 import com.google.gerrit.server.update.PostUpdateContext;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.time.TimeUtil;
@@ -319,9 +321,11 @@
       }
 
       Op o = new Op(PatchSet.id(cd.getId(), metadata.patchSet), parsedComments, message.id());
-      BatchUpdate batchUpdate = buf.create(project, ctx.getUser(), TimeUtil.now());
-      batchUpdate.addOp(cd.getId(), o);
-      batchUpdate.execute();
+      try (RefUpdateContext updCtx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        BatchUpdate batchUpdate = buf.create(project, ctx.getUser(), TimeUtil.now());
+        batchUpdate.addOp(cd.getId(), o);
+        batchUpdate.execute();
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
index ce54708..0eaafb8 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoySauceLoader.java
@@ -90,7 +90,7 @@
     "RevertedHtml.soy",
   };
 
-  private static final SoySauce DEFAULT = getDefault().build().compileTemplates();
+  private static final SoySauce DEFAULT = getDefault(null).build().compileTemplates();
 
   private final SitePaths site;
   private final PluginSetContext<MailSoyTemplateProvider> templateProviders;
@@ -106,7 +106,7 @@
       return DEFAULT;
     }
 
-    SoyFileSet.Builder builder = getDefault();
+    SoyFileSet.Builder builder = getDefault(site);
     templateProviders.runEach(
         e -> e.getFileNames().forEach(p -> addTemplate(builder, site, e.getPath(), p)));
     return builder.build().compileTemplates();
@@ -124,10 +124,10 @@
     }
   }
 
-  private static SoyFileSet.Builder getDefault() {
+  private static SoyFileSet.Builder getDefault(@Nullable SitePaths site) {
     SoyFileSet.Builder builder = SoyFileSet.builder();
     for (String name : TEMPLATES) {
-      addTemplate(builder, null, "com/google/gerrit/server/mail/", name);
+      addTemplate(builder, site, "com/google/gerrit/server/mail/", name);
     }
     return builder;
   }
diff --git a/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java b/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
index 5417494..0289e17 100644
--- a/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AllUsersAsyncUpdate.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
@@ -25,6 +26,7 @@
 import com.google.gerrit.server.FanOutExecutor;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.Map;
@@ -90,26 +92,28 @@
     Future<?> possiblyIgnoredError =
         executor.submit(
             () -> {
-              try (OpenRepo allUsersRepo = OpenRepo.open(repoManager, allUsersName)) {
-                allUsersRepo.addUpdatesNoLimits(draftUpdates);
-                allUsersRepo.flush();
-                BatchRefUpdate bru = allUsersRepo.repo.getRefDatabase().newBatchUpdate();
-                bru.setPushCertificate(pushCert);
-                if (refLogMessage != null) {
-                  bru.setRefLogMessage(refLogMessage, false);
-                } else {
-                  bru.setRefLogMessage(
-                      firstNonNull(NoteDbUtil.guessRestApiHandler(), "Update NoteDb refs async"),
-                      false);
+              try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+                try (OpenRepo allUsersRepo = OpenRepo.open(repoManager, allUsersName)) {
+                  allUsersRepo.addUpdatesNoLimits(draftUpdates);
+                  allUsersRepo.flush();
+                  BatchRefUpdate bru = allUsersRepo.repo.getRefDatabase().newBatchUpdate();
+                  bru.setPushCertificate(pushCert);
+                  if (refLogMessage != null) {
+                    bru.setRefLogMessage(refLogMessage, false);
+                  } else {
+                    bru.setRefLogMessage(
+                        firstNonNull(NoteDbUtil.guessRestApiHandler(), "Update NoteDb refs async"),
+                        false);
+                  }
+                  bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent);
+                  bru.setAtomic(true);
+                  allUsersRepo.cmds.addTo(bru);
+                  bru.setAllowNonFastForwards(true);
+                  RefUpdateUtil.executeChecked(bru, allUsersRepo.rw);
+                } catch (IOException e) {
+                  logger.atSevere().withCause(e).log(
+                      "Failed to delete draft comments asynchronously after publishing them");
                 }
-                bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent);
-                bru.setAtomic(true);
-                allUsersRepo.cmds.addTo(bru);
-                bru.setAllowNonFastForwards(true);
-                RefUpdateUtil.executeChecked(bru, allUsersRepo.rw);
-              } catch (IOException e) {
-                logger.atSevere().withCause(e).log(
-                    "Failed to delete draft comments asynchronously after publishing them");
               }
             });
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index ef62f2e..f8c7426 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -42,6 +42,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_WORK_IN_PROGRESS;
 import static com.google.gerrit.server.notedb.NoteDbUtil.sanitizeFooter;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.util.Comparator.naturalOrder;
 import static java.util.Objects.requireNonNull;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -79,6 +80,7 @@
 import com.google.gerrit.server.account.ServiceUserClassifier;
 import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.AttentionSetUtil;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gerrit.server.validators.ValidationException;
@@ -248,9 +250,11 @@
   }
 
   public ObjectId commit() throws IOException {
-    try (NoteDbUpdateManager updateManager = updateManagerFactory.create(getProjectName())) {
-      updateManager.add(this);
-      updateManager.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (NoteDbUpdateManager updateManager = updateManagerFactory.create(getProjectName())) {
+        updateManager.add(this);
+        updateManager.execute();
+      }
     }
     return getResult();
   }
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index 4d71d84..f4262e7 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_REAL_USER;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMITTED_WITH;
 import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TAG;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_PATTERN;
 import static com.google.gerrit.server.util.AccountTemplateUtil.ACCOUNT_TEMPLATE_REGEX;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -48,6 +49,7 @@
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.notedb.ChangeNoteUtil.AttentionStatusInNoteDb;
 import com.google.gerrit.server.notedb.ChangeNoteUtil.CommitMessageRange;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.AccountTemplateUtil;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
@@ -341,13 +343,15 @@
     if (refsUpdate == null) {
       return;
     }
-    if (!refsUpdate.batchRefUpdate().getCommands().isEmpty()) {
-      if (!options.dryRun) {
-        refsUpdate.inserter().flush();
-        RefUpdateUtil.executeChecked(refsUpdate.batchRefUpdate(), refsUpdate.revWalk());
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      if (!refsUpdate.batchRefUpdate().getCommands().isEmpty()) {
+        if (!options.dryRun) {
+          refsUpdate.inserter().flush();
+          RefUpdateUtil.executeChecked(refsUpdate.batchRefUpdate(), refsUpdate.revWalk());
+        }
       }
+      refsUpdate.close();
     }
-    refsUpdate.close();
   }
 
   /**
diff --git a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
index c8d93f8..3f3ede1 100644
--- a/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
+++ b/java/com/google/gerrit/server/notedb/DeleteZombieCommentsRefs.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.gerrit.entities.RefNames.REFS_DRAFT_COMMENTS;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static org.eclipse.jgit.lib.Constants.EMPTY_TREE_ID;
 
 import com.google.auto.value.AutoValue;
@@ -36,6 +37,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -402,17 +404,19 @@
 
   private void deleteBatchZombieRefs(Repository allUsersRepo, List<Ref> refsBatch)
       throws IOException {
-    List<ReceiveCommand> deleteCommands =
-        refsBatch.stream()
-            .map(
-                zombieRef ->
-                    new ReceiveCommand(
-                        zombieRef.getObjectId(), ObjectId.zeroId(), zombieRef.getName()))
-            .collect(toImmutableList());
-    BatchRefUpdate bru = allUsersRepo.getRefDatabase().newBatchUpdate();
-    bru.setAtomic(true);
-    bru.addCommand(deleteCommands);
-    RefUpdateUtil.executeChecked(bru, allUsersRepo);
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      List<ReceiveCommand> deleteCommands =
+          refsBatch.stream()
+              .map(
+                  zombieRef ->
+                      new ReceiveCommand(
+                          zombieRef.getObjectId(), ObjectId.zeroId(), zombieRef.getName()))
+              .collect(toImmutableList());
+      BatchRefUpdate bru = allUsersRepo.getRefDatabase().newBatchUpdate();
+      bru.setAtomic(true);
+      bru.addCommand(deleteCommands);
+      RefUpdateUtil.executeChecked(bru, allUsersRepo);
+    }
   }
 
   private List<Ref> filterZombieRefs(Repository allUsersRepo, List<Ref> allDraftRefs)
diff --git a/java/com/google/gerrit/server/notedb/RepoSequence.java b/java/com/google/gerrit/server/notedb/RepoSequence.java
index d743921..9aaac19 100644
--- a/java/com/google/gerrit/server/notedb/RepoSequence.java
+++ b/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.entities.RefNames.REFS;
 import static com.google.gerrit.entities.RefNames.REFS_SEQUENCES;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.REPO_SEQ;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -39,6 +40,7 @@
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
@@ -265,29 +267,31 @@
    * @param count the number of sequence numbers which should be retrieved
    */
   private void acquire(int count) {
-    try (Repository repo = repoManager.openRepository(projectName);
-        RevWalk rw = new RevWalk(repo)) {
-      logger.atFine().log("acquire %d ids on %s in %s", count, refName, projectName);
-      Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
-      afterReadRef.run();
-      ObjectId oldId;
-      int next;
-      if (!blob.isPresent()) {
-        oldId = ObjectId.zeroId();
-        next = seed.get();
-      } else {
-        oldId = blob.get().id();
-        next = blob.get().value();
+    try (RefUpdateContext ctx = RefUpdateContext.open(REPO_SEQ)) {
+      try (Repository repo = repoManager.openRepository(projectName);
+          RevWalk rw = new RevWalk(repo)) {
+        logger.atFine().log("acquire %d ids on %s in %s", count, refName, projectName);
+        Optional<IntBlob> blob = IntBlob.parse(repo, refName, rw);
+        afterReadRef.run();
+        ObjectId oldId;
+        int next;
+        if (!blob.isPresent()) {
+          oldId = ObjectId.zeroId();
+          next = seed.get();
+        } else {
+          oldId = blob.get().id();
+          next = blob.get().value();
+        }
+        next = Math.max(floor, next);
+        RefUpdate refUpdate =
+            IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
+        RefUpdateUtil.checkResult(refUpdate);
+        counter = next;
+        limit = counter + count;
+        acquireCount++;
+      } catch (IOException e) {
+        throw new StorageException(e);
       }
-      next = Math.max(floor, next);
-      RefUpdate refUpdate =
-          IntBlob.tryStore(repo, rw, projectName, refName, oldId, next + count, gitRefUpdated);
-      RefUpdateUtil.checkResult(refUpdate);
-      counter = next;
-      limit = counter + count;
-      acquireCount++;
-    } catch (IOException e) {
-      throw new StorageException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java b/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
index 60dff84..e91f7b7 100644
--- a/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
+++ b/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
@@ -72,13 +72,15 @@
     if (!ready) {
       synchronized (dataDir) {
         if (!ready) {
-          try {
-            Files.createDirectories(dataDir);
-          } catch (IOException e) {
-            throw new ProvisionException(
-                String.format(
-                    "Cannot create %s for plugin %s", dataDir.toAbsolutePath(), plugin.getName()),
-                e);
+          if (!Files.isDirectory(dataDir)) {
+            try {
+              Files.createDirectories(dataDir);
+            } catch (IOException e) {
+              throw new ProvisionException(
+                  String.format(
+                      "Cannot create %s for plugin %s", dataDir.toAbsolutePath(), plugin.getName()),
+                  e);
+            }
           }
           ready = true;
         }
diff --git a/java/com/google/gerrit/server/project/ProjectCreator.java b/java/com/google/gerrit/server/project/ProjectCreator.java
index f1c161d..485d926 100644
--- a/java/com/google/gerrit/server/project/ProjectCreator.java
+++ b/java/com/google/gerrit/server/project/ProjectCreator.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.project;
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
@@ -43,6 +44,7 @@
 import com.google.gerrit.server.git.RepositoryExistsException;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -105,36 +107,38 @@
 
   public ProjectState createProject(CreateProjectArgs args)
       throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
-    final Project.NameKey nameKey = args.getProject();
-    try {
-      final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0);
-      Status status = repoManager.getRepositoryStatus(nameKey);
-      if (!status.equals(Status.NON_EXISTENT)) {
-        throw new RepositoryExistsException(nameKey, "Repository status: " + status);
-      }
-      try (Repository repo = repoManager.createRepository(nameKey)) {
-        RefUpdate u = repo.updateRef(Constants.HEAD);
-        u.disableRefLog();
-        u.link(head);
-
-        createProjectConfig(args);
-
-        if (!args.permissionsOnly && args.createEmptyCommit) {
-          createEmptyCommits(repo, nameKey, args.branch);
+    try (RefUpdateContext ctx = RefUpdateContext.open(INIT_REPO)) {
+      final Project.NameKey nameKey = args.getProject();
+      try {
+        final String head = args.permissionsOnly ? RefNames.REFS_CONFIG : args.branch.get(0);
+        Status status = repoManager.getRepositoryStatus(nameKey);
+        if (!status.equals(Status.NON_EXISTENT)) {
+          throw new RepositoryExistsException(nameKey, "Repository status: " + status);
         }
+        try (Repository repo = repoManager.createRepository(nameKey)) {
+          RefUpdate u = repo.updateRef(Constants.HEAD);
+          u.disableRefLog();
+          u.link(head);
 
-        fire(nameKey, head);
+          createProjectConfig(args);
 
-        return projectCache.get(nameKey).orElseThrow(illegalState(nameKey));
+          if (!args.permissionsOnly && args.createEmptyCommit) {
+            createEmptyCommits(repo, nameKey, args.branch);
+          }
+
+          fire(nameKey, head);
+
+          return projectCache.get(nameKey).orElseThrow(illegalState(nameKey));
+        }
+      } catch (RepositoryExistsException e) {
+        throw new ResourceConflictException(
+            "Cannot create "
+                + nameKey.get()
+                + " because the name is already occupied by another project.",
+            e);
+      } catch (RepositoryNotFoundException badName) {
+        throw new BadRequestException("invalid project name: " + nameKey, badName);
       }
-    } catch (RepositoryExistsException e) {
-      throw new ResourceConflictException(
-          "Cannot create "
-              + nameKey.get()
-              + " because the name is already occupied by another project.",
-          e);
-    } catch (RepositoryNotFoundException badName) {
-      throw new BadRequestException("invalid project name: " + nameKey, badName);
     }
   }
 
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 738eab3..f6fc8db 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -225,9 +225,11 @@
   public static final String ARG_ID_GROUP = "group";
   public static final String ARG_ID_OWNER = "owner";
   public static final String ARG_ID_NON_UPLOADER = "non_uploader";
+  public static final String ARG_ID_NON_CONTRIBUTOR = "non_contributor";
   public static final String ARG_COUNT = "count";
   public static final Account.Id OWNER_ACCOUNT_ID = Account.id(0);
   public static final Account.Id NON_UPLOADER_ACCOUNT_ID = Account.id(-1);
+  public static final Account.Id NON_CONTRIBUTOR_ACCOUNT_ID = Account.id(-2);
 
   public static final String OPERATOR_MERGED_BEFORE = "mergedbefore";
   public static final String OPERATOR_MERGED_AFTER = "mergedafter";
@@ -485,13 +487,13 @@
     }
   }
 
-  private final Arguments args;
+  protected final Arguments args;
   protected Map<String, String> hasOperandAliases = Collections.emptyMap();
   private Map<Account.Id, DestinationList> destinationListByAccount = new HashMap<>();
 
   private static final Splitter RULE_SPLITTER = Splitter.on("=");
   private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
-  private static final Splitter LABEL_SPLITTER = Splitter.on(",");
+  protected static final Splitter LABEL_SPLITTER = Splitter.on(",");
 
   @Inject
   protected ChangeQueryBuilder(Arguments args) {
@@ -1038,6 +1040,8 @@
             accounts = Collections.singleton(OWNER_ACCOUNT_ID);
           } else if (value.equals(ARG_ID_NON_UPLOADER)) {
             accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
+          } else if (value.equals(ARG_ID_NON_CONTRIBUTOR)) {
+            accounts = Collections.singleton(NON_CONTRIBUTOR_ACCOUNT_ID);
           } else {
             accounts = parseAccount(value);
           }
@@ -1072,6 +1076,8 @@
             accounts = Collections.singleton(OWNER_ACCOUNT_ID);
           } else if (value.equals(ARG_ID_NON_UPLOADER)) {
             accounts = Collections.singleton(NON_UPLOADER_ACCOUNT_ID);
+          } else if (value.equals(ARG_ID_NON_CONTRIBUTOR)) {
+            accounts = Collections.singleton(NON_CONTRIBUTOR_ACCOUNT_ID);
           } else {
             accounts = parseAccount(value);
           }
@@ -1106,9 +1112,16 @@
       }
     }
 
+    validateLabelArgs(accounts);
     return new LabelPredicate(args, name, accounts, group, count, countOp);
   }
 
+  protected void validateLabelArgs(Set<Account.Id> accounts) throws QueryParseException {
+    if (accounts != null && accounts.contains(NON_CONTRIBUTOR_ACCOUNT_ID)) {
+      throw new QueryParseException("non_contributor arg is not allowed in change queries");
+    }
+  }
+
   /** Assert that keys {@code k1} and {@code k2} do not exist in {@code labelArgs} together. */
   private void assertDisjunctive(PredicateArgs labelArgs, String k1, String k2)
       throws QueryParseException {
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 5662e4d..83dd5ba 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
@@ -23,6 +24,8 @@
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -30,9 +33,15 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData.StorageConstraint;
+import java.io.IOException;
+import java.util.List;
 import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 
 public class EqualsLabelPredicate extends ChangeIndexPostFilterPredicate {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  protected final AccountResolver accountResolver;
   protected final ProjectCache projectCache;
   protected final PermissionBackend permissionBackend;
   protected final IdentifiedUser.GenericFactory userFactory;
@@ -61,6 +70,7 @@
       @Nullable Integer count) {
     super(ChangeField.LABEL_SPEC, ChangeField.formatLabel(label, expVal, account, count));
     this.permissionBackend = args.permissionBackend;
+    this.accountResolver = args.accountResolver;
     this.projectCache = args.projectCache;
     this.userFactory = args.userFactory;
     this.count = count;
@@ -155,6 +165,14 @@
           && cd.currentPatchSet().uploader().equals(approver)) {
         return false;
       }
+
+      if (account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID)) {
+        if ((cd.currentPatchSet().uploader().equals(approver)
+            || matchAccount(cd.getCommitter().getEmailAddress(), approver)
+            || matchAccount(cd.getAuthor().getEmailAddress(), approver))) {
+          return false;
+        }
+      }
     }
 
     IdentifiedUser reviewer = userFactory.create(approver);
@@ -176,9 +194,24 @@
     }
   }
 
+  /**
+   * Returns true if the {@code email} parameter belongs to the account identified by the {@code
+   * accountId} parameter.
+   */
+  private boolean matchAccount(String email, Account.Id accountId) {
+    try {
+      List<AccountState> accountsList = accountResolver.resolve(email).asList();
+      return accountsList.stream().anyMatch(c -> c.account().id().equals(accountId));
+    } catch (ConfigInvalidException | IOException e) {
+      logger.atWarning().withCause(e).log("Failed to resolve account %s", email);
+    }
+    return false;
+  }
+
   private boolean isMagicUser() {
     return account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
-        || account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID);
+        || account.equals(ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID)
+        || account.equals(ChangeQueryBuilder.NON_CONTRIBUTOR_ACCOUNT_ID);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/LabelPredicate.java b/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 2a5a47d..d89940d 100644
--- a/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.index.query.RangeUtil;
 import com.google.gerrit.index.query.RangeUtil.Range;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
@@ -36,6 +37,7 @@
   protected static final int MAX_COUNT = 5; // inclusive
 
   protected static class Args {
+    protected final AccountResolver accountResolver;
     protected final ProjectCache projectCache;
     protected final PermissionBackend permissionBackend;
     protected final IdentifiedUser.GenericFactory userFactory;
@@ -46,6 +48,7 @@
     protected final PredicateArgs.Operator countOp;
 
     protected Args(
+        AccountResolver accountResolver,
         ProjectCache projectCache,
         PermissionBackend permissionBackend,
         IdentifiedUser.GenericFactory userFactory,
@@ -54,6 +57,7 @@
         AccountGroup.UUID group,
         @Nullable Integer count,
         @Nullable PredicateArgs.Operator countOp) {
+      this.accountResolver = accountResolver;
       this.projectCache = projectCache;
       this.permissionBackend = permissionBackend;
       this.userFactory = userFactory;
@@ -89,6 +93,7 @@
     super(
         predicates(
             new Args(
+                a.accountResolver,
                 a.projectCache,
                 a.permissionBackend,
                 a.userFactory,
diff --git a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
index 5632c14..cb92ddd 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRequirementChangeQueryBuilder.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.base.Splitter;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
 import com.google.gerrit.index.query.Predicate;
 import com.google.gerrit.index.query.QueryBuilder;
@@ -30,6 +31,7 @@
 import com.google.inject.Inject;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
@@ -179,6 +181,9 @@
     return fileEditsPredicateFactory.create(FileEditsArgs.create(filePattern, contentPattern));
   }
 
+  @Override
+  protected void validateLabelArgs(Set<Account.Id> accountIds) throws QueryParseException {}
+
   private static void validateRegularExpression(String pattern, String errorMessage)
       throws QueryParseException {
     try {
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index 62da2f2..dd0ec78d 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -34,6 +34,7 @@
         "//lib/auto:auto-factory",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/commons:codec",
         "//lib/commons:compress",
         "//lib/commons:lang3",
         "//lib/errorprone:annotations",
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
index 2ae3166..9e02592 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftCommentsUtil.java
@@ -14,6 +14,7 @@
 package com.google.gerrit.server.restapi.account;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
@@ -43,6 +44,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -106,10 +108,13 @@
       update.addOp(cd.getId(), op);
       ops.add(op);
     }
-    // Currently there's no way to let some updates succeed even if others fail. Even if there were,
-    // all updates from this operation only happen in All-Users and thus are fully atomic, so
-    // allowing partial failure would have little value.
-    BatchUpdate.execute(updates.values(), ImmutableList.of(), false);
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      // Currently there's no way to let some updates succeed even if others fail. Even if there
+      // were,
+      // all updates from this operation only happen in All-Users and thus are fully atomic, so
+      // allowing partial failure would have little value.
+      BatchUpdate.execute(updates.values(), ImmutableList.of(), false);
+    }
     return ops.stream().map(Op::getResult).filter(Objects::nonNull).collect(toImmutableList());
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index 8dd0e78..36080a4 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -36,6 +38,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -126,16 +129,18 @@
     AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
     AbandonOp op = abandonOpFactory.create(accountState, msgTxt);
     ChangeData changeData = changeDataFactory.create(notes.getProjectName(), notes.getChangeId());
-    try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.now())) {
-      u.setNotify(notify);
-      u.addOp(notes.getChangeId(), op);
-      u.addOp(
-          notes.getChangeId(),
-          storeSubmitRequirementsOpFactory.create(
-              changeData.submitRequirements().values(), changeData));
-      u.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.now())) {
+        u.setNotify(notify);
+        u.addOp(notes.getChangeId(), op);
+        u.addOp(
+            notes.getChangeId(),
+            storeSubmitRequirementsOpFactory.create(
+                changeData.submitRequirements().values(), changeData));
+        u.execute();
+      }
+      return op.getChange();
     }
-    return op.getChange();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
index 03d383f..155e66f 100644
--- a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
+++ b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -32,6 +34,7 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.AttentionSetUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -87,17 +90,18 @@
         .test(ChangePermission.READ)) {
       throw new AuthException("read not permitted for " + attentionUserId);
     }
-
-    try (BatchUpdate bu =
-        updateFactory.create(
-            changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.now())) {
-      AddToAttentionSetOp op = opFactory.create(attentionUserId, input.reason, true);
-      bu.addOp(changeResource.getId(), op);
-      NotifyHandling notify = input.notify == null ? NotifyHandling.OWNER : input.notify;
-      NotifyResolver.Result notifyResult = notifyResolver.resolve(notify, input.notifyDetails);
-      bu.setNotify(notifyResult);
-      bu.execute();
-      return Response.ok(accountLoaderFactory.create(true).fillOne(attentionUserId));
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(
+              changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.now())) {
+        AddToAttentionSetOp op = opFactory.create(attentionUserId, input.reason, true);
+        bu.addOp(changeResource.getId(), op);
+        NotifyHandling notify = input.notify == null ? NotifyHandling.OWNER : input.notify;
+        NotifyResolver.Result notifyResult = notifyResolver.resolve(notify, input.notifyDetails);
+        bu.setNotify(notifyResult);
+        bu.execute();
+        return Response.ok(accountLoaderFactory.create(true).fillOne(attentionUserId));
+      }
     }
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
index 044fa0d..b6a106a 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
@@ -47,6 +49,7 @@
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -179,13 +182,15 @@
       ChangeNotes destNotes,
       CodeReviewCommit commit)
       throws IOException, UpdateException, RestApiException {
-    Change destChange = destNotes.getChange();
-    PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
-    PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, commit);
-    inserter.setMessage(buildMessageForPatchSet(psId));
-    bu.addOp(destChange.getId(), inserter);
-    bu.execute();
-    return inserter.getChange();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      Change destChange = destNotes.getChange();
+      PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
+      PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, commit);
+      inserter.setMessage(buildMessageForPatchSet(psId));
+      bu.addOp(destChange.getId(), inserter);
+      bu.execute();
+      return inserter.getChange();
+    }
   }
 
   private static String buildMessageForPatchSet(PatchSet.Id psId) {
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
index d4f549a..4021f77 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
@@ -23,6 +23,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
+import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.api.errors.PatchApplyException;
 import org.eclipse.jgit.api.errors.PatchFormatException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -51,8 +52,12 @@
       throws IOException, RestApiException {
     checkNotNull(mergeTip);
     RevTree tip = mergeTip.getTree();
-    InputStream patchStream =
-        new ByteArrayInputStream(input.patch.getBytes(StandardCharsets.UTF_8));
+    InputStream patchStream;
+    if (Base64.isBase64(input.patch)) {
+      patchStream = new ByteArrayInputStream(org.eclipse.jgit.util.Base64.decode(input.patch));
+    } else {
+      patchStream = new ByteArrayInputStream(input.patch.getBytes(StandardCharsets.UTF_8));
+    }
     try {
       PatchApplier applier = new PatchApplier(repo, tip, oi);
       PatchApplier.Result applyResult = applier.applyPatch(patchStream);
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index b688e2d..6fd75de 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -283,6 +283,7 @@
   /** Put handler that is activated when PUT request is called on collection element. */
   @Singleton
   public static class Put implements RestModifyView<ChangeEditResource, FileContentInput> {
+
     private static final Pattern BINARY_DATA_PATTERN =
         Pattern.compile("data:([\\w/.-]*);([\\w]+),(.*)");
     private static final String BASE64 = "base64";
@@ -348,7 +349,6 @@
                   + ") was invalid: supported values are 0, 644, or 755.");
         }
       }
-
       try (Repository repository = repositoryManager.openRepository(rsrc.getProject())) {
         editModifier.modifyFile(
             repository, rsrc.getNotes(), path, newContent, fileContentInput.fileMode);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index c192500..1bfb6bd 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
@@ -62,6 +63,7 @@
 import com.google.gerrit.server.submit.MergeIdenticalTreeException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -331,52 +333,53 @@
       } catch (MergeIdenticalTreeException | MergeConflictException e) {
         throw new IntegrationConflictException("Cherry pick failed: " + e.getMessage(), e);
       }
-
-      try (BatchUpdate bu = batchUpdateFactory.create(project, identifiedUser, timestamp)) {
-        bu.setRepository(git, revWalk, oi);
-        bu.setNotify(resolveNotify(input));
-        Change.Id changeId;
-        String newTopic = null;
-        if (input.topic != null) {
-          newTopic = Strings.emptyToNull(input.topic.trim());
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        try (BatchUpdate bu = batchUpdateFactory.create(project, identifiedUser, timestamp)) {
+          bu.setRepository(git, revWalk, oi);
+          bu.setNotify(resolveNotify(input));
+          Change.Id changeId;
+          String newTopic = null;
+          if (input.topic != null) {
+            newTopic = Strings.emptyToNull(input.topic.trim());
+          }
+          if (newTopic == null
+              && sourceChange != null
+              && !Strings.isNullOrEmpty(sourceChange.getTopic())) {
+            newTopic = sourceChange.getTopic() + "-" + dest.shortName();
+          }
+          if (destChange != null) {
+            // The change key exists on the destination branch. The cherry pick
+            // will be added as a new patch set.
+            changeId =
+                insertPatchSet(
+                    bu,
+                    git,
+                    destChange.notes(),
+                    cherryPickCommit,
+                    sourceChange,
+                    newTopic,
+                    input,
+                    workInProgress);
+          } else {
+            // Change key not found on destination branch. We can create a new
+            // change.
+            changeId =
+                createNewChange(
+                    bu,
+                    cherryPickCommit,
+                    dest.branch(),
+                    newTopic,
+                    project,
+                    sourceChange,
+                    sourceCommit,
+                    input,
+                    revertedChange,
+                    idForNewChange,
+                    workInProgress);
+          }
+          bu.execute();
+          return Result.create(changeId, cherryPickCommit.getFilesWithGitConflicts());
         }
-        if (newTopic == null
-            && sourceChange != null
-            && !Strings.isNullOrEmpty(sourceChange.getTopic())) {
-          newTopic = sourceChange.getTopic() + "-" + dest.shortName();
-        }
-        if (destChange != null) {
-          // The change key exists on the destination branch. The cherry pick
-          // will be added as a new patch set.
-          changeId =
-              insertPatchSet(
-                  bu,
-                  git,
-                  destChange.notes(),
-                  cherryPickCommit,
-                  sourceChange,
-                  newTopic,
-                  input,
-                  workInProgress);
-        } else {
-          // Change key not found on destination branch. We can create a new
-          // change.
-          changeId =
-              createNewChange(
-                  bu,
-                  cherryPickCommit,
-                  dest.branch(),
-                  newTopic,
-                  project,
-                  sourceChange,
-                  sourceCommit,
-                  input,
-                  revertedChange,
-                  idForNewChange,
-                  workInProgress);
-        }
-        bu.execute();
-        return Result.create(changeId, cherryPickCommit.getFilesWithGitConflicts());
       }
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 2cb427a..36dec86 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
 import com.google.common.base.Joiner;
@@ -80,6 +81,7 @@
 import com.google.gerrit.server.restapi.project.ProjectsCollection;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -329,102 +331,122 @@
       BatchUpdate.Factory updateFactory)
       throws RestApiException, PermissionBackendException, IOException, ConfigInvalidException,
           UpdateException {
-    logger.atFine().log(
-        "Creating new change for target branch %s in project %s"
-            + " (new branch = %s, base change = %s, base commit = %s)",
-        input.branch, projectState.getName(), input.newBranch, input.baseChange, input.baseCommit);
-
-    try (Repository git = gitManager.openRepository(projectState.getNameKey());
-        ObjectInserter oi = git.newObjectInserter();
-        ObjectReader reader = oi.newReader();
-        CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(reader)) {
-      PatchSet basePatchSet = null;
-      List<String> groups = Collections.emptyList();
-
-      if (input.baseChange != null) {
-        ChangeNotes baseChange = getBaseChange(input.baseChange);
-        basePatchSet = psUtil.current(baseChange);
-        groups = basePatchSet.groups();
-        logger.atFine().log("base patch set = %s (groups = %s)", basePatchSet.id(), groups);
-      }
-
-      ObjectId parentCommit =
-          getParentCommit(
-              git, rw, input.branch, input.newBranch, basePatchSet, input.baseCommit, input.merge);
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
       logger.atFine().log(
-          "parent commit = %s", parentCommit != null ? parentCommit.name() : "NULL");
+          "Creating new change for target branch %s in project %s"
+              + " (new branch = %s, base change = %s, base commit = %s)",
+          input.branch,
+          projectState.getName(),
+          input.newBranch,
+          input.baseChange,
+          input.baseCommit);
 
-      RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit);
+      try (Repository git = gitManager.openRepository(projectState.getNameKey());
+          ObjectInserter oi = git.newObjectInserter();
+          ObjectReader reader = oi.newReader();
+          CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(reader)) {
+        PatchSet basePatchSet = null;
+        List<String> groups = Collections.emptyList();
 
-      Instant now = TimeUtil.now();
-
-      PersonIdent committer = me.newCommitterIdent(now, serverZoneId);
-      PersonIdent author =
-          input.author == null
-              ? committer
-              : new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
-
-      String commitMessage = getCommitMessage(input.subject, me);
-
-      CodeReviewCommit c;
-      if (input.merge != null) {
-        // create a merge commit
-        c =
-            newMergeCommit(
-                git, oi, rw, projectState, mergeTip, input.merge, author, committer, commitMessage);
-        if (!c.getFilesWithGitConflicts().isEmpty()) {
-          logger.atFine().log(
-              "merge commit has conflicts in the following files: %s",
-              c.getFilesWithGitConflicts());
+        if (input.baseChange != null) {
+          ChangeNotes baseChange = getBaseChange(input.baseChange);
+          basePatchSet = psUtil.current(baseChange);
+          groups = basePatchSet.groups();
+          logger.atFine().log("base patch set = %s (groups = %s)", basePatchSet.id(), groups);
         }
-      } else if (input.patch != null) {
-        // create a commit with the given patch.
-        if (mergeTip == null) {
-          throw new BadRequestException("Cannot apply patch on top of an empty tree.");
+
+        ObjectId parentCommit =
+            getParentCommit(
+                git,
+                rw,
+                input.branch,
+                input.newBranch,
+                basePatchSet,
+                input.baseCommit,
+                input.merge);
+        logger.atFine().log(
+            "parent commit = %s", parentCommit != null ? parentCommit.name() : "NULL");
+
+        RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit);
+
+        Instant now = TimeUtil.now();
+
+        PersonIdent committer = me.newCommitterIdent(now, serverZoneId);
+        PersonIdent author =
+            input.author == null
+                ? committer
+                : new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
+
+        String commitMessage = getCommitMessage(input.subject, me);
+
+        CodeReviewCommit c;
+        if (input.merge != null) {
+          // create a merge commit
+          c =
+              newMergeCommit(
+                  git,
+                  oi,
+                  rw,
+                  projectState,
+                  mergeTip,
+                  input.merge,
+                  author,
+                  committer,
+                  commitMessage);
+          if (!c.getFilesWithGitConflicts().isEmpty()) {
+            logger.atFine().log(
+                "merge commit has conflicts in the following files: %s",
+                c.getFilesWithGitConflicts());
+          }
+        } else if (input.patch != null) {
+          // create a commit with the given patch.
+          if (mergeTip == null) {
+            throw new BadRequestException("Cannot apply patch on top of an empty tree.");
+          }
+          ObjectId treeId = ApplyPatchUtil.applyPatch(git, oi, input.patch, mergeTip);
+          c =
+              rw.parseCommit(
+                  CommitUtil.createCommitWithTree(
+                      oi, author, committer, mergeTip, commitMessage, treeId));
+        } else {
+          // create an empty commit.
+          c = createEmptyCommit(oi, rw, author, committer, mergeTip, commitMessage);
         }
-        ObjectId treeId = ApplyPatchUtil.applyPatch(git, oi, input.patch, mergeTip);
-        c =
-            rw.parseCommit(
-                CommitUtil.createCommitWithTree(
-                    oi, author, committer, mergeTip, commitMessage, treeId));
-      } else {
-        // create an empty commit.
-        c = createEmptyCommit(oi, rw, author, committer, mergeTip, commitMessage);
-      }
-      // Flush inserter so that commit becomes visible to validators
-      oi.flush();
+        // Flush inserter so that commit becomes visible to validators
+        oi.flush();
 
-      Change.Id changeId = Change.id(seq.nextChangeId());
-      ChangeInserter ins = changeInserterFactory.create(changeId, c, input.branch);
-      ins.setMessage(messageForNewChange(ins.getPatchSetId(), c));
-      ins.setTopic(input.topic);
-      ins.setPrivate(input.isPrivate);
-      ins.setWorkInProgress(input.workInProgress || !c.getFilesWithGitConflicts().isEmpty());
-      ins.setGroups(groups);
+        Change.Id changeId = Change.id(seq.nextChangeId());
+        ChangeInserter ins = changeInserterFactory.create(changeId, c, input.branch);
+        ins.setMessage(messageForNewChange(ins.getPatchSetId(), c));
+        ins.setTopic(input.topic);
+        ins.setPrivate(input.isPrivate);
+        ins.setWorkInProgress(input.workInProgress || !c.getFilesWithGitConflicts().isEmpty());
+        ins.setGroups(groups);
 
-      if (input.validationOptions != null) {
-        ImmutableListMultimap.Builder<String, String> validationOptions =
-            ImmutableListMultimap.builder();
-        input
-            .validationOptions
-            .entrySet()
-            .forEach(e -> validationOptions.put(e.getKey(), e.getValue()));
-        ins.setValidationOptions(validationOptions.build());
-      }
+        if (input.validationOptions != null) {
+          ImmutableListMultimap.Builder<String, String> validationOptions =
+              ImmutableListMultimap.builder();
+          input
+              .validationOptions
+              .entrySet()
+              .forEach(e -> validationOptions.put(e.getKey(), e.getValue()));
+          ins.setValidationOptions(validationOptions.build());
+        }
 
-      try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
-        bu.setRepository(git, rw, oi);
-        bu.setNotify(
-            notifyResolver.resolve(
-                firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
-        bu.insertChange(ins);
-        bu.execute();
+        try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
+          bu.setRepository(git, rw, oi);
+          bu.setNotify(
+              notifyResolver.resolve(
+                  firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
+          bu.insertChange(ins);
+          bu.execute();
+        }
+        ChangeInfo changeInfo = jsonFactory.noOptions().format(ins.getChange());
+        changeInfo.containsGitConflicts = !c.getFilesWithGitConflicts().isEmpty() ? true : null;
+        return changeInfo;
+      } catch (InvalidMergeStrategyException | MergeWithConflictsNotSupportedException e) {
+        throw new BadRequestException(e.getMessage());
       }
-      ChangeInfo changeInfo = jsonFactory.noOptions().format(ins.getChange());
-      changeInfo.containsGitConflicts = !c.getFilesWithGitConflicts().isEmpty() ? true : null;
-      return changeInfo;
-    } catch (InvalidMergeStrategyException | MergeWithConflictsNotSupportedException e) {
-      throw new BadRequestException(e.getMessage());
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index 9e9cf6a..cd0025f 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.entities.HumanComment;
@@ -36,6 +37,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -81,13 +83,15 @@
       throw new BadRequestException(
           String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
     }
-
-    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      Op op = new Op(rsrc.getPatchSet().id(), in);
-      bu.addOp(rsrc.getChange().getId(), op);
-      bu.execute();
-      return Response.created(
-          commentJson.get().setFillAccounts(false).newHumanCommentFormatter().format(op.comment));
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
+        Op op = new Op(rsrc.getPatchSet().id(), in);
+        bu.addOp(rsrc.getChange().getId(), op);
+        bu.execute();
+        return Response.created(
+            commentJson.get().setFillAccounts(false).newHumanCommentFormatter().format(op.comment));
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 4b66cdc..51094b7 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
@@ -63,6 +64,7 @@
 import com.google.gerrit.server.submit.MergeIdenticalTreeException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -200,18 +202,20 @@
       PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.id());
       PatchSetInserter psInserter =
           patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
-      try (BatchUpdate bu = updateFactory.create(project, me, now)) {
-        bu.setRepository(git, rw, oi);
-        bu.setNotify(NotifyResolver.Result.none());
-        psInserter
-            .setMessage(messageForChange(nextPsId, newCommit))
-            .setWorkInProgress(!newCommit.getFilesWithGitConflicts().isEmpty())
-            .setCheckAddPatchSetPermission(false);
-        if (groups != null) {
-          psInserter.setGroups(groups);
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        try (BatchUpdate bu = updateFactory.create(project, me, now)) {
+          bu.setRepository(git, rw, oi);
+          bu.setNotify(NotifyResolver.Result.none());
+          psInserter
+              .setMessage(messageForChange(nextPsId, newCommit))
+              .setWorkInProgress(!newCommit.getFilesWithGitConflicts().isEmpty())
+              .setCheckAddPatchSetPermission(false);
+          if (groups != null) {
+            psInserter.setGroups(groups);
+          }
+          bu.addOp(rsrc.getId(), psInserter);
+          bu.execute();
         }
-        bu.addOp(rsrc.getId(), psInserter);
-        bu.execute();
       }
 
       ChangeJson json = jsonFactory.create(ListChangesOption.CURRENT_REVISION);
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index 8298abb..9153703 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.Input;
@@ -30,6 +31,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -53,11 +55,13 @@
       throw new MethodNotAllowedException("delete not permitted");
     }
     rsrc.permissions().check(ChangePermission.DELETE);
-
-    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      Change.Id id = rsrc.getChange().getId();
-      bu.addOp(id, opFactory.create(id));
-      bu.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
+        Change.Id id = rsrc.getChange().getId();
+        bu.addOp(id, opFactory.create(id));
+        bu.execute();
+      }
     }
     return Response.none();
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index 588d56e..ca6bfad 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.AccountTemplateUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -88,9 +90,11 @@
         createNewChangeMessage(user.asIdentifiedUser().getAccountId(), input.reason);
     DeleteChangeMessageOp deleteChangeMessageOp =
         new DeleteChangeMessageOp(resource.getChangeMessageId(), newChangeMessage);
-    try (BatchUpdate batchUpdate =
-        updateFactory.create(resource.getChangeResource().getProject(), user, TimeUtil.now())) {
-      batchUpdate.addOp(resource.getChangeId(), deleteChangeMessageOp).execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate batchUpdate =
+          updateFactory.create(resource.getChangeResource().getProject(), user, TimeUtil.now())) {
+        batchUpdate.addOp(resource.getChangeId(), deleteChangeMessageOp).execute();
+      }
     }
 
     ChangeMessageInfo updatedMessageInfo =
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteComment.java b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
index 2056664..1397582 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.PatchSet;
@@ -35,6 +37,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -83,9 +86,13 @@
 
     String newMessage = getCommentNewMessage(user.asIdentifiedUser().getName(), input.reason);
     DeleteCommentOp deleteCommentOp = new DeleteCommentOp(rsrc, newMessage);
-    try (BatchUpdate batchUpdate =
-        updateFactory.create(rsrc.getRevisionResource().getProject(), user, TimeUtil.now())) {
-      batchUpdate.addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp).execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate batchUpdate =
+          updateFactory.create(rsrc.getRevisionResource().getProject(), user, TimeUtil.now())) {
+        batchUpdate
+            .addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp)
+            .execute();
+      }
     }
 
     ChangeNotes updatedNotes =
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index 7d28a39..f55e9c7 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
 import com.google.gerrit.entities.PatchSet;
@@ -30,6 +32,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -53,11 +56,13 @@
   @Override
   public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input)
       throws RestApiException, UpdateException {
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
-      Op op = new Op(rsrc.getComment().key);
-      bu.addOp(rsrc.getChange().getId(), op);
-      bu.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
+        Op op = new Op(rsrc.getComment().key);
+        bu.addOp(rsrc.getChange().getId(), op);
+        bu.execute();
+      }
     }
     return Response.none();
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
index 08725b5..5c63bd7 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.InputWithMessage;
@@ -30,6 +31,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -62,8 +64,11 @@
     }
 
     SetPrivateOp op = setPrivateOpFactory.create(false, input);
-    try (BatchUpdate u = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      u.addOp(rsrc.getId(), op).execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate u =
+          updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
+        u.addOp(rsrc.getId(), op).execute();
+      }
     }
 
     return Response.none();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
index 7a409e8..cbc3b5e 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -27,6 +29,7 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -53,21 +56,22 @@
     if (input == null) {
       input = new DeleteReviewerInput();
     }
-
-    try (BatchUpdate bu =
-        updateFactory.create(
-            rsrc.getChangeResource().getProject(),
-            rsrc.getChangeResource().getUser(),
-            TimeUtil.now())) {
-      bu.setNotify(getNotify(rsrc.getChange(), input));
-      BatchUpdateOp op;
-      if (rsrc.isByEmail()) {
-        op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail());
-      } else {
-        op = deleteReviewerOpFactory.create(rsrc.getReviewerUser().getAccount(), input);
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(
+              rsrc.getChangeResource().getProject(),
+              rsrc.getChangeResource().getUser(),
+              TimeUtil.now())) {
+        bu.setNotify(getNotify(rsrc.getChange(), input));
+        BatchUpdateOp op;
+        if (rsrc.isByEmail()) {
+          op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail());
+        } else {
+          op = deleteReviewerOpFactory.create(rsrc.getReviewerUser().getAccount(), input);
+        }
+        bu.addOp(rsrc.getChange().getId(), op);
+        bu.execute();
       }
-      bu.addOp(rsrc.getChange().getId(), op);
-      bu.execute();
     }
     return Response.none();
   }
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 9fa3160..b3d7fa2 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
@@ -32,6 +33,7 @@
 import com.google.gerrit.server.change.VoteResource;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -80,34 +82,37 @@
     if (r.getRevisionResource() != null && !r.getRevisionResource().isCurrent()) {
       throw new MethodNotAllowedException("Cannot delete vote on non-current patch set");
     }
-
-    try (BatchUpdate bu =
-        updateFactory.create(
-            change.getProject(), r.getChangeResource().getUser(), TimeUtil.now())) {
-      bu.setNotify(
-          notifyResolver.resolve(
-              firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
-      bu.addOp(
-          change.getId(),
-          deleteVoteOpFactory.create(
-              r.getChange().getProject(),
-              r.getReviewerUser().state(),
-              rsrc.getLabel(),
-              input,
-              true));
-      if (!input.ignoreAutomaticAttentionSetRules
-          && !r.getReviewerUser().getAccountId().equals(currentUserProvider.get().getAccountId())) {
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(
+              change.getProject(), r.getChangeResource().getUser(), TimeUtil.now())) {
+        bu.setNotify(
+            notifyResolver.resolve(
+                firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
         bu.addOp(
             change.getId(),
-            attentionSetOpFactory.create(
-                r.getReviewerUser().getAccountId(),
-                /* reason= */ "Their vote was deleted",
-                /* notify= */ false));
+            deleteVoteOpFactory.create(
+                r.getChange().getProject(),
+                r.getReviewerUser().state(),
+                rsrc.getLabel(),
+                input,
+                true));
+        if (!input.ignoreAutomaticAttentionSetRules
+            && !r.getReviewerUser()
+                .getAccountId()
+                .equals(currentUserProvider.get().getAccountId())) {
+          bu.addOp(
+              change.getId(),
+              attentionSetOpFactory.create(
+                  r.getReviewerUser().getAccountId(),
+                  /* reason= */ "Their vote was deleted",
+                  /* notify= */ false));
+        }
+        if (input.ignoreAutomaticAttentionSetRules) {
+          bu.addOp(change.getId(), new AttentionSetUnchangedOp());
+        }
+        bu.execute();
       }
-      if (input.ignoreAutomaticAttentionSetRules) {
-        bu.addOp(change.getId(), new AttentionSetUnchangedOp());
-      }
-      bu.execute();
     }
 
     return Response.none();
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index c1b36d7..94f9b8d 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
@@ -59,6 +60,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -156,9 +158,11 @@
     projectCache.get(project).orElseThrow(illegalState(project)).checkStatePermitsWrite();
 
     Op op = new Op(input);
-    try (BatchUpdate u = updateFactory.create(project, caller, TimeUtil.now())) {
-      u.addOp(change.getId(), op);
-      u.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate u = updateFactory.create(project, caller, TimeUtil.now())) {
+        u.addOp(change.getId(), op);
+        u.execute();
+      }
     }
     return Response.ok(json.noOptions().format(op.getChange()));
   }
diff --git a/java/com/google/gerrit/server/restapi/change/PostHashtags.java b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
index bcaa145..a503eda 100644
--- a/java/com/google/gerrit/server/restapi/change/PostHashtags.java
+++ b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
 import com.google.gerrit.extensions.restapi.Response;
@@ -26,6 +28,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -46,13 +49,14 @@
   public Response<ImmutableSortedSet<String>> apply(ChangeResource req, HashtagsInput input)
       throws RestApiException, UpdateException, PermissionBackendException {
     req.permissions().check(ChangePermission.EDIT_HASHTAGS);
-
-    try (BatchUpdate bu =
-        updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.now())) {
-      SetHashtagsOp op = hashtagsFactory.create(input);
-      bu.addOp(req.getId(), op);
-      bu.execute();
-      return Response.ok(op.getUpdatedHashtags());
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.now())) {
+        SetHashtagsOp op = hashtagsFactory.create(input);
+        bu.addOp(req.getId(), op);
+        bu.execute();
+        return Response.ok(op.getUpdatedHashtags());
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
index 45d7250..56b81b8 100644
--- a/java/com/google/gerrit/server/restapi/change/PostPrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.common.InputWithMessage;
@@ -33,6 +34,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -74,8 +76,11 @@
     }
 
     SetPrivateOp op = setPrivateOpFactory.create(true, input);
-    try (BatchUpdate u = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      u.addOp(rsrc.getId(), op).execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate u =
+          updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
+        u.addOp(rsrc.getId(), op).execute();
+      }
     }
 
     return Response.created();
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 22eb32c..9940637 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toList;
@@ -104,6 +105,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -302,91 +304,93 @@
 
     // Notify based on ReviewInput, ignoring the notify settings from any ReviewerInputs.
     NotifyResolver.Result notify = notifyResolver.resolve(input.notify, input.notifyDetails);
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(revision.getChange().getProject(), revision.getUser(), ts)) {
+        bu.setNotify(notify);
 
-    try (BatchUpdate bu =
-        updateFactory.create(revision.getChange().getProject(), revision.getUser(), ts)) {
-      bu.setNotify(notify);
-
-      Account account = revision.getUser().asIdentifiedUser().getAccount();
-      boolean ccOrReviewer = false;
-      if (input.labels != null && !input.labels.isEmpty()) {
-        ccOrReviewer = input.labels.values().stream().anyMatch(v -> v != 0);
-        if (ccOrReviewer) {
-          logger.atFine().log("calling user is cc/reviewer on the change due to voting on a label");
-        }
-      }
-
-      if (!ccOrReviewer) {
-        // Check if user was already CCed or reviewing prior to this review.
-        ReviewerSet currentReviewers =
-            approvalsUtil.getReviewers(revision.getChangeResource().getNotes());
-        ccOrReviewer = currentReviewers.all().contains(account.id());
-        if (ccOrReviewer) {
-          logger.atFine().log("calling user is already cc/reviewer on the change");
-        }
-      }
-
-      // Apply reviewer changes first. Revision emails should be sent to the
-      // updated set of reviewers. Also keep track of whether the user added
-      // themselves as a reviewer or to the CC list.
-      logger.atFine().log("adding reviewer additions");
-      for (ReviewerModification reviewerResult : reviewerResults) {
-        reviewerResult.op.suppressEmail(); // Send a single batch email below.
-        reviewerResult.op.suppressEvent(); // Send events below, if possible as batch.
-        bu.addOp(revision.getChange().getId(), reviewerResult.op);
-        if (!ccOrReviewer && reviewerResult.reviewers.contains(account)) {
-          logger.atFine().log("calling user is explicitly added as reviewer or CC");
-          ccOrReviewer = true;
-        }
-      }
-
-      if (!ccOrReviewer) {
-        // User posting this review isn't currently in the reviewer or CC list,
-        // isn't being explicitly added, and isn't voting on any label.
-        // Automatically CC them on this change so they receive replies.
-        logger.atFine().log("CCing calling user");
-        ReviewerModification selfAddition =
-            reviewerModifier.ccCurrentUser(revision.getUser(), revision);
-        selfAddition.op.suppressEmail();
-        selfAddition.op.suppressEvent();
-        bu.addOp(revision.getChange().getId(), selfAddition.op);
-      }
-
-      // Add WorkInProgressOp if requested.
-      if ((input.ready || input.workInProgress)
-          && didWorkInProgressChange(revision.getChange().isWorkInProgress(), input)) {
-        if (input.ready && input.workInProgress) {
-          output.error = ERROR_WIP_READY_MUTUALLY_EXCLUSIVE;
-          return Response.withStatusCode(SC_BAD_REQUEST, output);
+        Account account = revision.getUser().asIdentifiedUser().getAccount();
+        boolean ccOrReviewer = false;
+        if (input.labels != null && !input.labels.isEmpty()) {
+          ccOrReviewer = input.labels.values().stream().anyMatch(v -> v != 0);
+          if (ccOrReviewer) {
+            logger.atFine().log(
+                "calling user is cc/reviewer on the change due to voting on a label");
+          }
         }
 
-        revision
-            .getChangeResource()
-            .permissions()
-            .check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
-
-        if (input.ready) {
-          output.ready = true;
+        if (!ccOrReviewer) {
+          // Check if user was already CCed or reviewing prior to this review.
+          ReviewerSet currentReviewers =
+              approvalsUtil.getReviewers(revision.getChangeResource().getNotes());
+          ccOrReviewer = currentReviewers.all().contains(account.id());
+          if (ccOrReviewer) {
+            logger.atFine().log("calling user is already cc/reviewer on the change");
+          }
         }
 
-        logger.atFine().log("setting work-in-progress to %s", input.workInProgress);
-        WorkInProgressOp wipOp =
-            workInProgressOpFactory.create(input.workInProgress, new WorkInProgressOp.Input());
-        wipOp.suppressEmail();
-        bu.addOp(revision.getChange().getId(), wipOp);
+        // Apply reviewer changes first. Revision emails should be sent to the
+        // updated set of reviewers. Also keep track of whether the user added
+        // themselves as a reviewer or to the CC list.
+        logger.atFine().log("adding reviewer additions");
+        for (ReviewerModification reviewerResult : reviewerResults) {
+          reviewerResult.op.suppressEmail(); // Send a single batch email below.
+          reviewerResult.op.suppressEvent(); // Send events below, if possible as batch.
+          bu.addOp(revision.getChange().getId(), reviewerResult.op);
+          if (!ccOrReviewer && reviewerResult.reviewers.contains(account)) {
+            logger.atFine().log("calling user is explicitly added as reviewer or CC");
+            ccOrReviewer = true;
+          }
+        }
+
+        if (!ccOrReviewer) {
+          // User posting this review isn't currently in the reviewer or CC list,
+          // isn't being explicitly added, and isn't voting on any label.
+          // Automatically CC them on this change so they receive replies.
+          logger.atFine().log("CCing calling user");
+          ReviewerModification selfAddition =
+              reviewerModifier.ccCurrentUser(revision.getUser(), revision);
+          selfAddition.op.suppressEmail();
+          selfAddition.op.suppressEvent();
+          bu.addOp(revision.getChange().getId(), selfAddition.op);
+        }
+
+        // Add WorkInProgressOp if requested.
+        if ((input.ready || input.workInProgress)
+            && didWorkInProgressChange(revision.getChange().isWorkInProgress(), input)) {
+          if (input.ready && input.workInProgress) {
+            output.error = ERROR_WIP_READY_MUTUALLY_EXCLUSIVE;
+            return Response.withStatusCode(SC_BAD_REQUEST, output);
+          }
+
+          revision
+              .getChangeResource()
+              .permissions()
+              .check(ChangePermission.TOGGLE_WORK_IN_PROGRESS_STATE);
+
+          if (input.ready) {
+            output.ready = true;
+          }
+
+          logger.atFine().log("setting work-in-progress to %s", input.workInProgress);
+          WorkInProgressOp wipOp =
+              workInProgressOpFactory.create(input.workInProgress, new WorkInProgressOp.Input());
+          wipOp.suppressEmail();
+          bu.addOp(revision.getChange().getId(), wipOp);
+        }
+
+        // Add the review ops.
+        logger.atFine().log("posting review");
+        PostReviewOp postReviewOp =
+            postReviewOpFactory.create(
+                projectState, revision.getPatchSet().id(), input, revision.getAccountId());
+        bu.addOp(revision.getChange().getId(), postReviewOp);
+
+        // Adjust the attention set based on the input
+        replyAttentionSetUpdates.updateAttentionSet(
+            bu, revision.getNotes(), input, revision.getUser());
+        bu.execute();
       }
-
-      // Add the review ops.
-      logger.atFine().log("posting review");
-      PostReviewOp postReviewOp =
-          postReviewOpFactory.create(
-              projectState, revision.getPatchSet().id(), input, revision.getAccountId());
-      bu.addOp(revision.getChange().getId(), postReviewOp);
-
-      // Adjust the attention set based on the input
-      replyAttentionSetUpdates.updateAttentionSet(
-          bu, revision.getNotes(), input, revision.getUser());
-      bu.execute();
     }
 
     // Re-read change to take into account results of the update.
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 9bc80a4..e46f9e4 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewerInput;
@@ -31,6 +33,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -70,11 +73,14 @@
     if (modification.op == null) {
       return Response.ok(modification.result);
     }
-    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      bu.setNotify(resolveNotify(rsrc, input));
-      Change.Id id = rsrc.getChange().getId();
-      bu.addOp(id, modification.op);
-      bu.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
+        bu.setNotify(resolveNotify(rsrc, input));
+        Change.Id id = rsrc.getChange().getId();
+        bu.addOp(id, modification.op);
+        bu.execute();
+      }
     }
 
     // Re-read change to take into account results of the update.
diff --git a/java/com/google/gerrit/server/restapi/change/PutDescription.java b/java/com/google/gerrit/server/restapi/change/PutDescription.java
index 5b5bc15..0d633db 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDescription.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.common.DescriptionInput;
@@ -31,6 +33,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -56,10 +59,12 @@
     rsrc.permissions().check(ChangePermission.EDIT_DESCRIPTION);
 
     Op op = new Op(input != null ? input : new DescriptionInput(), rsrc.getPatchSet().id());
-    try (BatchUpdate u =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
-      u.addOp(rsrc.getChange().getId(), op);
-      u.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate u =
+          updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
+        u.addOp(rsrc.getChange().getId(), op);
+        u.execute();
+      }
     }
     return Strings.isNullOrEmpty(op.newDescription)
         ? Response.none()
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 6411087..681e1b1 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
@@ -36,6 +37,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -86,13 +88,15 @@
       throw new BadRequestException(
           String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
     }
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
-      Op op = new Op(rsrc.getComment().key, in);
-      bu.addOp(rsrc.getChange().getId(), op);
-      bu.execute();
-      return Response.ok(
-          commentJson.get().setFillAccounts(false).newHumanCommentFormatter().format(op.comment));
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
+        Op op = new Op(rsrc.getComment().key, in);
+        bu.addOp(rsrc.getChange().getId(), op);
+        bu.execute();
+        return Response.ok(
+            commentJson.get().setFillAccounts(false).newHumanCommentFormatter().format(op.comment));
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index f898dca..4a4f546 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.entities.BooleanProjectConfig;
 import com.google.gerrit.entities.PatchSet;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -127,21 +129,24 @@
       }
 
       Instant ts = TimeUtil.now();
-      try (BatchUpdate bu =
-          updateFactory.create(resource.getChange().getProject(), userProvider.get(), ts)) {
-        // Ensure that BatchUpdate will update the same repo
-        bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter);
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        try (BatchUpdate bu =
+            updateFactory.create(resource.getChange().getProject(), userProvider.get(), ts)) {
+          // Ensure that BatchUpdate will update the same repo
+          bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter);
 
-        PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.id());
-        ObjectId newCommit =
-            createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts);
-        PatchSetInserter inserter = psInserterFactory.create(resource.getNotes(), psId, newCommit);
-        inserter.setMessage(
-            String.format("Patch Set %s: Commit message was updated.", psId.getId()));
-        inserter.setDescription("Edit commit message");
-        bu.setNotify(resolveNotify(input, resource));
-        bu.addOp(resource.getChange().getId(), inserter);
-        bu.execute();
+          PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.id());
+          ObjectId newCommit =
+              createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts);
+          PatchSetInserter inserter =
+              psInserterFactory.create(resource.getNotes(), psId, newCommit);
+          inserter.setMessage(
+              String.format("Patch Set %s: Commit message was updated.", psId.getId()));
+          inserter.setDescription("Edit commit message");
+          bu.setNotify(resolveNotify(input, resource));
+          bu.addOp(resource.getChange().getId(), inserter);
+          bu.execute();
+        }
       }
     }
     return Response.ok("ok");
diff --git a/java/com/google/gerrit/server/restapi/change/PutTopic.java b/java/com/google/gerrit/server/restapi/change/PutTopic.java
index c9b436e..b1e5d5a 100644
--- a/java/com/google/gerrit/server/restapi/change/PutTopic.java
+++ b/java/com/google/gerrit/server/restapi/change/PutTopic.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.api.changes.TopicInput;
 import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -28,6 +30,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -62,10 +65,12 @@
     }
 
     SetTopicOp op = topicOpFactory.create(sanitizedInput.topic);
-    try (BatchUpdate u =
-        updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.now())) {
-      u.addOp(req.getId(), op);
-      u.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate u =
+          updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.now())) {
+        u.addOp(req.getId(), op);
+        u.execute();
+      }
     }
 
     if (Strings.isNullOrEmpty(sanitizedInput.topic)) {
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 6535e42..fd51fbc 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -52,6 +53,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -124,30 +126,32 @@
         .checkStatePermitsWrite();
 
     Change change = rsrc.getChange();
-    try (Repository repo = repoManager.openRepository(change.getProject());
-        ObjectInserter oi = repo.newObjectInserter();
-        ObjectReader reader = oi.newReader();
-        RevWalk rw = CodeReviewCommit.newRevWalk(reader);
-        BatchUpdate bu =
-            updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      rebaseUtil.verifyRebasePreconditions(rw, rsrc.getNotes(), rsrc.getPatchSet());
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (Repository repo = repoManager.openRepository(change.getProject());
+          ObjectInserter oi = repo.newObjectInserter();
+          ObjectReader reader = oi.newReader();
+          RevWalk rw = CodeReviewCommit.newRevWalk(reader);
+          BatchUpdate bu =
+              updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
+        rebaseUtil.verifyRebasePreconditions(rw, rsrc.getNotes(), rsrc.getPatchSet());
 
-      RebaseChangeOp rebaseOp =
-          rebaseUtil.getRebaseOp(
-              rsrc,
-              input,
-              rebaseUtil.parseOrFindBaseRevision(repo, rw, permissionBackend, rsrc, input, true));
+        RebaseChangeOp rebaseOp =
+            rebaseUtil.getRebaseOp(
+                rsrc,
+                input,
+                rebaseUtil.parseOrFindBaseRevision(repo, rw, permissionBackend, rsrc, input, true));
 
-      // TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
-      bu.setNotify(NotifyResolver.Result.none());
-      bu.setRepository(repo, rw, oi);
-      bu.addOp(change.getId(), rebaseOp);
-      bu.execute();
+        // TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
+        bu.setNotify(NotifyResolver.Result.none());
+        bu.setRepository(repo, rw, oi);
+        bu.addOp(change.getId(), rebaseOp);
+        bu.execute();
 
-      ChangeInfo changeInfo = json.create(OPTIONS).format(change.getProject(), change.getId());
-      changeInfo.containsGitConflicts =
-          !rebaseOp.getRebasedCommit().getFilesWithGitConflicts().isEmpty() ? true : null;
-      return Response.ok(changeInfo);
+        ChangeInfo changeInfo = json.create(OPTIONS).format(change.getProject(), change.getId());
+        changeInfo.containsGitConflicts =
+            !rebaseOp.getRebasedCommit().getFilesWithGitConflicts().isEmpty() ? true : null;
+        return Response.ok(changeInfo);
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
index 5ae496f..34a2623 100644
--- a/java/com/google/gerrit/server/restapi/change/RebaseChain.java
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
@@ -52,6 +53,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -128,66 +130,68 @@
 
     List<Change.Id> upToDateAncestors = new ArrayList<>();
     Map<Change.Id, RebaseChangeOp> rebaseOps = new LinkedHashMap<>();
-    try (Repository repo = repoManager.openRepository(project);
-        ObjectInserter oi = repo.newObjectInserter();
-        ObjectReader reader = oi.newReader();
-        RevWalk rw = CodeReviewCommit.newRevWalk(reader);
-        BatchUpdate bu = updateFactory.create(project, user, TimeUtil.now())) {
-      List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc);
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (Repository repo = repoManager.openRepository(project);
+          ObjectInserter oi = repo.newObjectInserter();
+          ObjectReader reader = oi.newReader();
+          RevWalk rw = CodeReviewCommit.newRevWalk(reader);
+          BatchUpdate bu = updateFactory.create(project, user, TimeUtil.now())) {
+        List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc);
 
-      boolean ancestorsAreUpToDate = true;
-      for (int i = 0; i < chain.size(); i++) {
-        ChangeData changeData = chain.get(i).data();
-        PatchSet ps = patchSetUtil.current(changeData.notes());
-        if (ps == null) {
-          throw new IllegalStateException(
-              "current revision is missing for change " + changeData.getId());
-        }
+        boolean ancestorsAreUpToDate = true;
+        for (int i = 0; i < chain.size(); i++) {
+          ChangeData changeData = chain.get(i).data();
+          PatchSet ps = patchSetUtil.current(changeData.notes());
+          if (ps == null) {
+            throw new IllegalStateException(
+                "current revision is missing for change " + changeData.getId());
+          }
 
-        RevisionResource revRsrc =
-            new RevisionResource(changeResourceFactory.create(changeData, user), ps);
-        revRsrc.permissions().check(ChangePermission.REBASE);
-        rebaseUtil.verifyRebasePreconditions(rw, changeData.notes(), ps);
+          RevisionResource revRsrc =
+              new RevisionResource(changeResourceFactory.create(changeData, user), ps);
+          revRsrc.permissions().check(ChangePermission.REBASE);
+          rebaseUtil.verifyRebasePreconditions(rw, changeData.notes(), ps);
 
-        boolean isUpToDate = false;
-        RebaseChangeOp rebaseOp = null;
-        if (i == 0) {
-          ObjectId desiredBase =
-              rebaseUtil.parseOrFindBaseRevision(
-                  repo, rw, permissionBackend, revRsrc, input, false);
-          if (currentBase(rw, ps).equals(desiredBase)) {
-            isUpToDate = true;
+          boolean isUpToDate = false;
+          RebaseChangeOp rebaseOp = null;
+          if (i == 0) {
+            ObjectId desiredBase =
+                rebaseUtil.parseOrFindBaseRevision(
+                    repo, rw, permissionBackend, revRsrc, input, false);
+            if (currentBase(rw, ps).equals(desiredBase)) {
+              isUpToDate = true;
+            } else {
+              rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, desiredBase);
+            }
           } else {
-            rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, desiredBase);
+            if (ancestorsAreUpToDate) {
+              ObjectId latestCommittedBase =
+                  PatchSetUtil.getCurrentCommittedRevCommit(
+                      project, rw, notesFactory, chain.get(i - 1).id());
+              isUpToDate = currentBase(rw, ps).equals(latestCommittedBase);
+            }
+            if (!isUpToDate) {
+              rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, chain.get(i - 1).id());
+            }
           }
-        } else {
-          if (ancestorsAreUpToDate) {
-            ObjectId latestCommittedBase =
-                PatchSetUtil.getCurrentCommittedRevCommit(
-                    project, rw, notesFactory, chain.get(i - 1).id());
-            isUpToDate = currentBase(rw, ps).equals(latestCommittedBase);
+
+          if (isUpToDate) {
+            upToDateAncestors.add(changeData.getId());
+            continue;
           }
-          if (!isUpToDate) {
-            rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, chain.get(i - 1).id());
-          }
+          ancestorsAreUpToDate = false;
+          bu.addOp(revRsrc.getChange().getId(), rebaseOp);
+          rebaseOps.put(revRsrc.getChange().getId(), rebaseOp);
         }
 
-        if (isUpToDate) {
-          upToDateAncestors.add(changeData.getId());
-          continue;
+        if (ancestorsAreUpToDate) {
+          throw new ResourceConflictException("The whole chain is already up to date.");
         }
-        ancestorsAreUpToDate = false;
-        bu.addOp(revRsrc.getChange().getId(), rebaseOp);
-        rebaseOps.put(revRsrc.getChange().getId(), rebaseOp);
-      }
 
-      if (ancestorsAreUpToDate) {
-        throw new ResourceConflictException("The whole chain is already up to date.");
+        bu.setNotify(NotifyResolver.Result.none());
+        bu.setRepository(repo, rw, oi);
+        bu.execute();
       }
-
-      bu.setNotify(NotifyResolver.Result.none());
-      bu.setRepository(repo, rw, oi);
-      bu.execute();
     }
 
     RebaseChainInfo res = new RebaseChainInfo();
@@ -241,6 +245,9 @@
     try (Repository repo = repoManager.openRepository(tipRsrc.getProject());
         RevWalk rw = new RevWalk(repo)) {
       List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc);
+      if (chain.size() <= 1) {
+        return description;
+      }
       PatchSetData oldestAncestor = chain.get(0);
       if (rebaseUtil.canRebase(
           oldestAncestor.patchSet(), oldestAncestor.data().change().getDest(), repo, rw)) {
diff --git a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
index bd3e8ec..d761fa7 100644
--- a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
+++ b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.changes.AttentionSetInput;
@@ -30,6 +32,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.AttentionSetUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -79,16 +82,18 @@
       }
     }
     ChangeResource changeResource = attentionResource.getChangeResource();
-    try (BatchUpdate bu =
-        updateFactory.create(
-            changeResource.getProject(), changeResource.getUser(), TimeUtil.now())) {
-      RemoveFromAttentionSetOp op =
-          opFactory.create(attentionResource.getAccountId(), input.reason, true);
-      bu.addOp(changeResource.getId(), op);
-      NotifyHandling notify = input.notify == null ? NotifyHandling.OWNER : input.notify;
-      NotifyResolver.Result notifyResult = notifyResolver.resolve(notify, input.notifyDetails);
-      bu.setNotify(notifyResult);
-      bu.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(
+              changeResource.getProject(), changeResource.getUser(), TimeUtil.now())) {
+        RemoveFromAttentionSetOp op =
+            opFactory.create(attentionResource.getAccountId(), input.reason, true);
+        bu.addOp(changeResource.getId(), op);
+        NotifyHandling notify = input.notify == null ? NotifyHandling.OWNER : input.notify;
+        NotifyResolver.Result notifyResult = notifyResolver.resolve(notify, input.notifyDetails);
+        bu.setNotify(notifyResult);
+        bu.execute();
+      }
     }
     return Response.none();
   }
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index 19d0677..6ac9c21 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
@@ -47,6 +48,7 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.PostUpdateContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -99,11 +101,13 @@
         .checkStatePermitsWrite();
 
     Op op = new Op(input);
-    try (BatchUpdate u =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
-      u.addOp(rsrc.getId(), op).execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate u =
+          updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
+        u.addOp(rsrc.getId(), op).execute();
+      }
+      return Response.ok(json.noOptions().format(op.change));
     }
-    return Response.ok(json.noOptions().format(op.change));
   }
 
   private class Op implements BatchUpdateOp {
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 62fdcbb..4c7c352 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.server.permissions.ChangePermission.REVERT;
 import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.Strings;
@@ -70,6 +71,7 @@
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.CommitMessageUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
@@ -306,24 +308,26 @@
     cherryPickInput.message = revertInput.message;
     ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
     Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId());
-    try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.now())) {
-      bu.setNotify(
-          notifyResolver.resolve(
-              firstNonNull(cherryPickInput.notify, NotifyHandling.ALL),
-              cherryPickInput.notifyDetails));
-      bu.addOp(
-          changeNotes.getChange().getId(),
-          new CreateCherryPickOp(
-              revCommitId,
-              generatedChangeId,
-              cherryPickRevertChangeId,
-              timestamp,
-              revertInput.workInProgress));
-      if (!revertInput.workInProgress) {
-        commitUtil.addChangeRevertedNotificationOps(
-            bu, changeNotes.getChangeId(), cherryPickRevertChangeId, generatedChangeId.name());
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.now())) {
+        bu.setNotify(
+            notifyResolver.resolve(
+                firstNonNull(cherryPickInput.notify, NotifyHandling.ALL),
+                cherryPickInput.notifyDetails));
+        bu.addOp(
+            changeNotes.getChange().getId(),
+            new CreateCherryPickOp(
+                revCommitId,
+                generatedChangeId,
+                cherryPickRevertChangeId,
+                timestamp,
+                revertInput.workInProgress));
+        if (!revertInput.workInProgress) {
+          commitUtil.addChangeRevertedNotificationOps(
+              bu, changeNotes.getChangeId(), cherryPickRevertChangeId, generatedChangeId.name());
+        }
+        bu.execute();
       }
-      bu.execute();
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index a587ecc..7d3fe98 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -34,6 +35,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -68,16 +70,18 @@
     if (!change.isWorkInProgress()) {
       throw new ResourceConflictException("change is not work in progress");
     }
-
-    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.ALL)));
-      bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input));
-      if (change.getRevertOf() != null) {
-        commitUtil.addChangeRevertedNotificationOps(
-            bu, change.getRevertOf(), change.getId(), change.getKey().get());
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
+        bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.ALL)));
+        bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input));
+        if (change.getRevertOf() != null) {
+          commitUtil.addChangeRevertedNotificationOps(
+              bu, change.getRevertOf(), change.getId(), change.getKey().get());
+        }
+        bu.execute();
+        return Response.ok();
       }
-      bu.execute();
-      return Response.ok();
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index 0ad5180..306aeea 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -33,6 +34,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -62,12 +64,14 @@
     if (change.isWorkInProgress()) {
       throw new ResourceConflictException("change is already work in progress");
     }
-
-    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
-      bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.NONE)));
-      bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input));
-      bu.execute();
-      return Response.ok();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (BatchUpdate bu =
+          updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
+        bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.NONE)));
+        bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input));
+        bu.execute();
+        return Response.ok();
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 977bfdb..65182db 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -45,6 +46,7 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -142,24 +144,26 @@
       md.setMessage("Review access change");
       md.setInsertChangeId(true);
       Change.Id changeId = Change.id(seq.nextChangeId());
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        RevCommit commit =
+            config.commitToNewRef(
+                md, PatchSet.id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
 
-      RevCommit commit =
-          config.commitToNewRef(md, PatchSet.id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
+        if (commit.name().equals(oldCommitSha1)) {
+          throw new BadRequestException("no change");
+        }
 
-      if (commit.name().equals(oldCommitSha1)) {
-        throw new BadRequestException("no change");
-      }
-
-      try (ObjectInserter objInserter = md.getRepository().newObjectInserter();
-          ObjectReader objReader = objInserter.newReader();
-          RevWalk rw = new RevWalk(objReader);
-          BatchUpdate bu =
-              updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.now())) {
-        bu.setRepository(md.getRepository(), rw, objInserter);
-        ChangeInserter ins = newInserter(changeId, commit);
-        bu.insertChange(ins);
-        bu.execute();
-        return Response.created(jsonFactory.noOptions().format(ins.getChange()));
+        try (ObjectInserter objInserter = md.getRepository().newObjectInserter();
+            ObjectReader objReader = objInserter.newReader();
+            RevWalk rw = new RevWalk(objReader);
+            BatchUpdate bu =
+                updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.now())) {
+          bu.setRepository(md.getRepository(), rw, objInserter);
+          ChangeInserter ins = newInserter(changeId, commit);
+          bu.insertChange(ins);
+          bu.execute();
+          return Response.created(jsonFactory.noOptions().format(ins.getChange()));
+        }
       }
     } catch (InvalidNameException e) {
       throw new BadRequestException(e.toString());
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index 17fc6db..412559b 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import static com.google.gerrit.entities.RefNames.isConfigRef;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
 
 import com.google.common.base.Strings;
 import com.google.gerrit.entities.BranchNameKey;
@@ -42,6 +43,7 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.RefUtil;
 import com.google.gerrit.server.project.RefValidationHelper;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -87,130 +89,132 @@
       throws BadRequestException, AuthException, ResourceConflictException,
           UnprocessableEntityException, IOException, PermissionBackendException,
           NoSuchProjectException {
-    String ref = id.get();
-    if (input == null) {
-      input = new BranchInput();
-    }
-    if (input.ref != null && !ref.equals(input.ref)) {
-      throw new BadRequestException("ref must match URL");
-    }
-    if (input.revision != null) {
-      input.revision = input.revision.trim();
-    }
-    if (Strings.isNullOrEmpty(input.revision)) {
-      input.revision = Constants.HEAD;
-    }
-    while (ref.startsWith("/")) {
-      ref = ref.substring(1);
-    }
-    ref = RefNames.fullName(ref);
-    if (!Repository.isValidRefName(ref)) {
-      throw new BadRequestException("invalid branch name \"" + ref + "\"");
-    }
-    if (MagicBranch.isMagicBranch(ref)) {
-      throw new BadRequestException(
-          "not allowed to create branches under \""
-              + MagicBranch.getMagicRefNamePrefix(ref)
-              + "\"");
-    }
-    if (!isBranchAllowed(ref)) {
-      throw new BadRequestException(
-          "Cannot create a branch with name \""
-              + ref
-              + "\". Not allowed to create branches under Gerrit internal or tags refs.");
-    }
-
-    BranchNameKey name = BranchNameKey.create(rsrc.getNameKey(), ref);
-    try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
-      ObjectId revid = RefUtil.parseBaseRevision(repo, input.revision);
-      RevWalk rw = RefUtil.verifyConnected(repo, revid);
-      RevObject object = rw.parseAny(revid);
-
-      if (ref.startsWith(Constants.R_HEADS)) {
-        // Ensure that what we start the branch from is a commit. If we
-        // were given a tag, dereference to the commit instead.
-        //
-        object = rw.parseCommit(object);
+    try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
+      String ref = id.get();
+      if (input == null) {
+        input = new BranchInput();
+      }
+      if (input.ref != null && !ref.equals(input.ref)) {
+        throw new BadRequestException("ref must match URL");
+      }
+      if (input.revision != null) {
+        input.revision = input.revision.trim();
+      }
+      if (Strings.isNullOrEmpty(input.revision)) {
+        input.revision = Constants.HEAD;
+      }
+      while (ref.startsWith("/")) {
+        ref = ref.substring(1);
+      }
+      ref = RefNames.fullName(ref);
+      if (!Repository.isValidRefName(ref)) {
+        throw new BadRequestException("invalid branch name \"" + ref + "\"");
+      }
+      if (MagicBranch.isMagicBranch(ref)) {
+        throw new BadRequestException(
+            "not allowed to create branches under \""
+                + MagicBranch.getMagicRefNamePrefix(ref)
+                + "\"");
+      }
+      if (!isBranchAllowed(ref)) {
+        throw new BadRequestException(
+            "Cannot create a branch with name \""
+                + ref
+                + "\". Not allowed to create branches under Gerrit internal or tags refs.");
       }
 
-      Ref sourceRef = repo.exactRef(input.revision);
-      if (sourceRef == null) {
-        createRefControl.checkCreateRef(identifiedUser, repo, name, object, /* forPush= */ false);
-      } else {
-        if (sourceRef.isSymbolic()) {
-          sourceRef = sourceRef.getTarget();
+      BranchNameKey name = BranchNameKey.create(rsrc.getNameKey(), ref);
+      try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) {
+        ObjectId revid = RefUtil.parseBaseRevision(repo, input.revision);
+        RevWalk rw = RefUtil.verifyConnected(repo, revid);
+        RevObject object = rw.parseAny(revid);
+
+        if (ref.startsWith(Constants.R_HEADS)) {
+          // Ensure that what we start the branch from is a commit. If we
+          // were given a tag, dereference to the commit instead.
+          //
+          object = rw.parseCommit(object);
         }
-        createRefControl.checkCreateRef(
-            identifiedUser,
-            repo,
-            name,
-            object,
-            /* forPush= */ false,
-            BranchNameKey.create(rsrc.getNameKey(), sourceRef.getName()));
-      }
 
-      RefUpdate u = repo.updateRef(ref);
-      u.setExpectedOldObjectId(ObjectId.zeroId());
-      u.setNewObjectId(object.copy());
-      u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
-      u.setRefLogMessage("created via REST from " + input.revision, false);
-      refCreationValidator.validateRefOperation(
-          rsrc.getName(),
-          identifiedUser.get(),
-          u,
-          ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
-      RefUpdate.Result result = u.update(rw);
-      switch (result) {
-        case FAST_FORWARD:
-        case NEW:
-        case NO_CHANGE:
-          referenceUpdated.fire(
-              name.project(), u, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
-          break;
-        case LOCK_FAILURE:
-          if (repo.getRefDatabase().exactRef(ref) != null) {
-            throw new ResourceConflictException("branch \"" + ref + "\" already exists");
+        Ref sourceRef = repo.exactRef(input.revision);
+        if (sourceRef == null) {
+          createRefControl.checkCreateRef(identifiedUser, repo, name, object, /* forPush= */ false);
+        } else {
+          if (sourceRef.isSymbolic()) {
+            sourceRef = sourceRef.getTarget();
           }
-          String refPrefix = RefUtil.getRefPrefix(ref);
-          while (!Constants.R_HEADS.equals(refPrefix)) {
-            if (repo.getRefDatabase().exactRef(refPrefix) != null) {
-              throw new ResourceConflictException(
-                  "Cannot create branch \""
-                      + ref
-                      + "\" since it conflicts with branch \""
-                      + refPrefix
-                      + "\".");
+          createRefControl.checkCreateRef(
+              identifiedUser,
+              repo,
+              name,
+              object,
+              /* forPush= */ false,
+              BranchNameKey.create(rsrc.getNameKey(), sourceRef.getName()));
+        }
+
+        RefUpdate u = repo.updateRef(ref);
+        u.setExpectedOldObjectId(ObjectId.zeroId());
+        u.setNewObjectId(object.copy());
+        u.setRefLogIdent(identifiedUser.get().newRefLogIdent());
+        u.setRefLogMessage("created via REST from " + input.revision, false);
+        refCreationValidator.validateRefOperation(
+            rsrc.getName(),
+            identifiedUser.get(),
+            u,
+            ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
+        RefUpdate.Result result = u.update(rw);
+        switch (result) {
+          case FAST_FORWARD:
+          case NEW:
+          case NO_CHANGE:
+            referenceUpdated.fire(
+                name.project(), u, ReceiveCommand.Type.CREATE, identifiedUser.get().state());
+            break;
+          case LOCK_FAILURE:
+            if (repo.getRefDatabase().exactRef(ref) != null) {
+              throw new ResourceConflictException("branch \"" + ref + "\" already exists");
             }
-            refPrefix = RefUtil.getRefPrefix(refPrefix);
-          }
-          throw new LockFailureException(String.format("Failed to create %s", ref), u);
-        case FORCED:
-        case IO_FAILURE:
-        case NOT_ATTEMPTED:
-        case REJECTED:
-        case REJECTED_CURRENT_BRANCH:
-        case RENAMED:
-        case REJECTED_MISSING_OBJECT:
-        case REJECTED_OTHER_REASON:
-        default:
-          throw new IOException(String.format("Failed to create %s: %s", ref, result.name()));
-      }
+            String refPrefix = RefUtil.getRefPrefix(ref);
+            while (!Constants.R_HEADS.equals(refPrefix)) {
+              if (repo.getRefDatabase().exactRef(refPrefix) != null) {
+                throw new ResourceConflictException(
+                    "Cannot create branch \""
+                        + ref
+                        + "\" since it conflicts with branch \""
+                        + refPrefix
+                        + "\".");
+              }
+              refPrefix = RefUtil.getRefPrefix(refPrefix);
+            }
+            throw new LockFailureException(String.format("Failed to create %s", ref), u);
+          case FORCED:
+          case IO_FAILURE:
+          case NOT_ATTEMPTED:
+          case REJECTED:
+          case REJECTED_CURRENT_BRANCH:
+          case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
+          default:
+            throw new IOException(String.format("Failed to create %s: %s", ref, result.name()));
+        }
 
-      BranchInfo info = new BranchInfo();
-      info.ref = ref;
-      info.revision = revid.getName();
+        BranchInfo info = new BranchInfo();
+        info.ref = ref;
+        info.revision = revid.getName();
 
-      if (isConfigRef(name.branch())) {
-        // Never allow to delete the meta config branch.
-        info.canDelete = null;
-      } else {
-        info.canDelete =
-            permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
-                    && rsrc.getProjectState().statePermitsWrite()
-                ? true
-                : null;
+        if (isConfigRef(name.branch())) {
+          // Never allow to delete the meta config branch.
+          info.canDelete = null;
+        } else {
+          info.canDelete =
+              permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
+                      && rsrc.getProjectState().statePermitsWrite()
+                  ? true
+                  : null;
+        }
+        return Response.created(info);
       }
-      return Response.created(info);
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index 63734bb..34c3ff7 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.TAG_MODIFICATION;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.common.base.Strings;
@@ -39,6 +40,7 @@
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.RefUtil;
 import com.google.gerrit.server.project.TagResource;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -97,64 +99,66 @@
     ref = RefUtil.normalizeTagRef(ref);
     PermissionBackend.ForRef perm =
         permissionBackend.currentUser().project(resource.getNameKey()).ref(ref);
+    try (RefUpdateContext ctx = RefUpdateContext.open(TAG_MODIFICATION)) {
+      try (Repository repo = repoManager.openRepository(resource.getNameKey())) {
+        ObjectId revid = RefUtil.parseBaseRevision(repo, input.revision);
+        RevWalk rw = RefUtil.verifyConnected(repo, revid);
+        // Reachability through tags does not influence a commit's visibility, so no need to check
+        // for
+        // visibility.
+        RevObject object = rw.parseAny(revid);
+        rw.reset();
+        boolean isAnnotated = Strings.emptyToNull(input.message) != null;
+        boolean isSigned = isAnnotated && input.message.contains("-----BEGIN PGP SIGNATURE-----\n");
+        if (isSigned) {
+          throw new MethodNotAllowedException("Cannot create signed tag \"" + ref + "\"");
+        } else if (isAnnotated) {
+          if (!check(perm, RefPermission.CREATE_TAG)) {
+            throw new AuthException("Cannot create annotated tag \"" + ref + "\"");
+          }
 
-    try (Repository repo = repoManager.openRepository(resource.getNameKey())) {
-      ObjectId revid = RefUtil.parseBaseRevision(repo, input.revision);
-      RevWalk rw = RefUtil.verifyConnected(repo, revid);
-      // Reachability through tags does not influence a commit's visibility, so no need to check for
-      // visibility.
-      RevObject object = rw.parseAny(revid);
-      rw.reset();
-      boolean isAnnotated = Strings.emptyToNull(input.message) != null;
-      boolean isSigned = isAnnotated && input.message.contains("-----BEGIN PGP SIGNATURE-----\n");
-      if (isSigned) {
-        throw new MethodNotAllowedException("Cannot create signed tag \"" + ref + "\"");
-      } else if (isAnnotated) {
-        if (!check(perm, RefPermission.CREATE_TAG)) {
-          throw new AuthException("Cannot create annotated tag \"" + ref + "\"");
+        } else {
+          perm.check(RefPermission.CREATE);
+        }
+        if (repo.getRefDatabase().exactRef(ref) != null) {
+          throw new ResourceConflictException("tag \"" + ref + "\" already exists");
         }
 
-      } else {
-        perm.check(RefPermission.CREATE);
-      }
-      if (repo.getRefDatabase().exactRef(ref) != null) {
-        throw new ResourceConflictException("tag \"" + ref + "\" already exists");
-      }
+        try (Git git = new Git(repo)) {
+          TagCommand tag =
+              git.tag()
+                  .setObjectId(object)
+                  .setName(ref.substring(R_TAGS.length()))
+                  .setAnnotated(isAnnotated)
+                  .setSigned(isSigned);
 
-      try (Git git = new Git(repo)) {
-        TagCommand tag =
-            git.tag()
-                .setObjectId(object)
-                .setName(ref.substring(R_TAGS.length()))
-                .setAnnotated(isAnnotated)
-                .setSigned(isSigned);
+          if (isAnnotated) {
+            tag.setMessage(input.message)
+                .setTagger(
+                    resource
+                        .getUser()
+                        .asIdentifiedUser()
+                        .newCommitterIdent(TimeUtil.now(), ZoneId.systemDefault()));
+          }
 
-        if (isAnnotated) {
-          tag.setMessage(input.message)
-              .setTagger(
-                  resource
-                      .getUser()
-                      .asIdentifiedUser()
-                      .newCommitterIdent(TimeUtil.now(), ZoneId.systemDefault()));
+          Ref result = tag.call();
+          tagCache.updateFastForward(
+              resource.getNameKey(), ref, ObjectId.zeroId(), result.getObjectId());
+          referenceUpdated.fire(
+              resource.getNameKey(),
+              ref,
+              ObjectId.zeroId(),
+              result.getObjectId(),
+              resource.getUser().asIdentifiedUser().state());
+          try (RevWalk w = new RevWalk(repo)) {
+            return Response.created(
+                ListTags.createTagInfo(perm, result, w, resource.getProjectState(), links));
+          }
         }
-
-        Ref result = tag.call();
-        tagCache.updateFastForward(
-            resource.getNameKey(), ref, ObjectId.zeroId(), result.getObjectId());
-        referenceUpdated.fire(
-            resource.getNameKey(),
-            ref,
-            ObjectId.zeroId(),
-            result.getObjectId(),
-            resource.getUser().asIdentifiedUser().state());
-        try (RevWalk w = new RevWalk(repo)) {
-          return Response.created(
-              ListTags.createTagInfo(perm, result, w, resource.getProjectState(), links));
-        }
+      } catch (GitAPIException e) {
+        logger.atSevere().withCause(e).log("Cannot create tag \"%s\"", ref);
+        throw new IOException(e);
       }
-    } catch (GitAPIException e) {
-      logger.atSevere().withCause(e).log("Cannot create tag \"%s\"", ref);
-      throw new IOException(e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
index 6248a61..227a01b0 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.project;
 
 import static com.google.gerrit.entities.RefNames.isConfigRef;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
 import com.google.gerrit.entities.RefNames;
@@ -27,6 +28,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -58,8 +60,9 @@
     if (!queryProvider.get().setLimit(1).byBranchOpen(rsrc.getBranchKey()).isEmpty()) {
       throw new ResourceConflictException("branch " + rsrc.getBranchKey() + " has open changes");
     }
-
-    deleteRef.deleteSingleRef(rsrc.getProjectState(), rsrc.getRef(), R_HEADS);
+    try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
+      deleteRef.deleteSingleRef(rsrc.getProjectState(), rsrc.getRef(), R_HEADS);
+    }
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
index ca5962e..a1b5f81e 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
 import com.google.common.collect.ImmutableSet;
@@ -26,6 +27,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -52,9 +54,10 @@
       // Never allow to delete the meta config branch.
       throw new MethodNotAllowedException("not allowed to delete branch " + RefNames.REFS_CONFIG);
     }
-
-    deleteRef.deleteMultipleRefs(
-        project.getProjectState(), ImmutableSet.copyOf(input.branches), R_HEADS);
+    try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
+      deleteRef.deleteMultipleRefs(
+          project.getProjectState(), ImmutableSet.copyOf(input.branches), R_HEADS);
+    }
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 5a84f69..b7fe46e 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.entities.RefNames.isConfigRef;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
 import static java.lang.String.format;
 import static org.eclipse.jgit.lib.Constants.R_REFS;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefValidationHelper;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -102,56 +104,58 @@
    */
   public void deleteSingleRef(ProjectState projectState, String ref, @Nullable String prefix)
       throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
-    if (prefix != null && !ref.startsWith(R_REFS)) {
-      ref = prefix + ref;
-    }
+    try (RefUpdateContext ctx = RefUpdateContext.open(BRANCH_MODIFICATION)) {
+      if (prefix != null && !ref.startsWith(R_REFS)) {
+        ref = prefix + ref;
+      }
 
-    projectState.checkStatePermitsWrite();
-    permissionBackend
-        .currentUser()
-        .project(projectState.getNameKey())
-        .ref(ref)
-        .check(RefPermission.DELETE);
+      projectState.checkStatePermitsWrite();
+      permissionBackend
+          .currentUser()
+          .project(projectState.getNameKey())
+          .ref(ref)
+          .check(RefPermission.DELETE);
 
-    try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
-      RefUpdate.Result result;
-      RefUpdate u = repository.updateRef(ref);
-      u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
-      u.setNewObjectId(ObjectId.zeroId());
-      u.setForceUpdate(true);
-      refDeletionValidator.validateRefOperation(
-          projectState.getName(),
-          identifiedUser.get(),
-          u,
-          /* pushOptions */ ImmutableListMultimap.of());
-      result = u.delete();
+      try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
+        RefUpdate.Result result;
+        RefUpdate u = repository.updateRef(ref);
+        u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
+        u.setNewObjectId(ObjectId.zeroId());
+        u.setForceUpdate(true);
+        refDeletionValidator.validateRefOperation(
+            projectState.getName(),
+            identifiedUser.get(),
+            u,
+            /* pushOptions */ ImmutableListMultimap.of());
+        result = u.delete();
 
-      switch (result) {
-        case NEW:
-        case NO_CHANGE:
-        case FAST_FORWARD:
-        case FORCED:
-          referenceUpdated.fire(
-              projectState.getNameKey(),
-              u,
-              ReceiveCommand.Type.DELETE,
-              identifiedUser.get().state());
-          break;
+        switch (result) {
+          case NEW:
+          case NO_CHANGE:
+          case FAST_FORWARD:
+          case FORCED:
+            referenceUpdated.fire(
+                projectState.getNameKey(),
+                u,
+                ReceiveCommand.Type.DELETE,
+                identifiedUser.get().state());
+            break;
 
-        case REJECTED_CURRENT_BRANCH:
-          logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name());
-          throw new ResourceConflictException("cannot delete current branch");
+          case REJECTED_CURRENT_BRANCH:
+            logger.atFine().log("Cannot delete current branch %s: %s", ref, result.name());
+            throw new ResourceConflictException("cannot delete current branch");
 
-        case LOCK_FAILURE:
-          throw new LockFailureException(String.format("Cannot delete %s", ref), u);
-        case IO_FAILURE:
-        case NOT_ATTEMPTED:
-        case REJECTED:
-        case RENAMED:
-        case REJECTED_MISSING_OBJECT:
-        case REJECTED_OTHER_REASON:
-        default:
-          throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name()));
+          case LOCK_FAILURE:
+            throw new LockFailureException(String.format("Cannot delete %s", ref), u);
+          case IO_FAILURE:
+          case NOT_ATTEMPTED:
+          case REJECTED:
+          case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
+          default:
+            throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name()));
+        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTag.java b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
index 8d0a3d3..e22c90f 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTag.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.TAG_MODIFICATION;
+
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.restapi.Response;
@@ -22,6 +24,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.RefUtil;
 import com.google.gerrit.server.project.TagResource;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -44,7 +47,9 @@
 
     Preconditions.checkState(tag.startsWith(Constants.R_TAGS));
 
-    deleteRef.deleteSingleRef(resource.getProjectState(), tag);
+    try (RefUpdateContext ctx = RefUpdateContext.open(TAG_MODIFICATION)) {
+      deleteRef.deleteSingleRef(resource.getProjectState(), tag);
+    }
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTags.java b/java/com/google/gerrit/server/restapi/project/DeleteTags.java
index 7ac3aff..a015d2b 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTags.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTags.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.project;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.TAG_MODIFICATION;
 import static org.eclipse.jgit.lib.Constants.R_TAGS;
 
 import com.google.common.collect.ImmutableSet;
@@ -24,6 +25,7 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -43,12 +45,14 @@
     if (input == null || input.tags == null || input.tags.isEmpty()) {
       throw new BadRequestException("tags must be specified");
     }
-
-    // If input.tags = ["refs/heads/bla"], this will actually delete the 'ref/heads/bla' branch,
-    // rather than refs/tags/refs/heads/bla.
-    // Since this is checked against DELETE permissions for refs/heads/bla, we'll let it go through.
-    deleteRef.deleteMultipleRefs(
-        project.getProjectState(), ImmutableSet.copyOf(input.tags), R_TAGS);
+    try (RefUpdateContext ctx = RefUpdateContext.open(TAG_MODIFICATION)) {
+      // If input.tags = ["refs/heads/bla"], this will actually delete the 'ref/heads/bla' branch,
+      // rather than refs/tags/refs/heads/bla.
+      // Since this is checked against DELETE permissions for refs/heads/bla, we'll let it go
+      // through.
+      deleteRef.deleteMultipleRefs(
+          project.getProjectState(), ImmutableSet.copyOf(input.tags), R_TAGS);
+    }
     return Response.none();
   }
 }
diff --git a/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 09f142b..dc83d4a 100644
--- a/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.schema.AclUtil.grant;
 import static com.google.gerrit.server.schema.AclUtil.rule;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
 
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -39,6 +40,7 @@
 import com.google.gerrit.server.notedb.RepoSequence;
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -84,18 +86,20 @@
   }
 
   public void create(AllProjectsInput input) throws IOException, ConfigInvalidException {
-    try (Repository git = repositoryManager.openRepository(allProjectsName)) {
-      initAllProjects(git, input);
-    } catch (RepositoryNotFoundException notFound) {
-      // A repository may be missing if this project existed only to store
-      // inheritable permissions. For example 'All-Projects'.
-      try (Repository git = repositoryManager.createRepository(allProjectsName)) {
+    try (RefUpdateContext updCtx = RefUpdateContext.open(INIT_REPO)) {
+      try (Repository git = repositoryManager.openRepository(allProjectsName)) {
         initAllProjects(git, input);
-        RefUpdate u = git.updateRef(Constants.HEAD);
-        u.link(RefNames.REFS_CONFIG);
-      } catch (RepositoryNotFoundException err) {
-        String name = allProjectsName.get();
-        throw new IOException("Cannot create repository " + name, err);
+      } catch (RepositoryNotFoundException notFound) {
+        // A repository may be missing if this project existed only to store
+        // inheritable permissions. For example 'All-Projects'.
+        try (Repository git = repositoryManager.createRepository(allProjectsName)) {
+          initAllProjects(git, input);
+          RefUpdate u = git.updateRef(Constants.HEAD);
+          u.link(RefNames.REFS_CONFIG);
+        } catch (RepositoryNotFoundException err) {
+          String name = allProjectsName.get();
+          throw new IOException("Cannot create repository " + name, err);
+        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/schema/AllUsersCreator.java b/java/com/google/gerrit/server/schema/AllUsersCreator.java
index f2fe7f6..63fbaf9 100644
--- a/java/com/google/gerrit/server/schema/AllUsersCreator.java
+++ b/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.schema.AclUtil.grant;
 import static com.google.gerrit.server.schema.AllProjectsInput.getDefaultCodeReviewLabel;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.UsedAt;
@@ -35,6 +36,7 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.RefPattern;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -90,16 +92,18 @@
   }
 
   public void create() throws IOException, ConfigInvalidException {
-    try (Repository git = mgr.openRepository(allUsersName)) {
-      initAllUsers(git);
-    } catch (RepositoryNotFoundException notFound) {
-      try (Repository git = mgr.createRepository(allUsersName)) {
+    try (RefUpdateContext ctx = RefUpdateContext.open(INIT_REPO)) {
+      try (Repository git = mgr.openRepository(allUsersName)) {
         initAllUsers(git);
-        RefUpdate u = git.updateRef(Constants.HEAD);
-        u.link(RefNames.REFS_CONFIG);
-      } catch (RepositoryNotFoundException err) {
-        String name = allUsersName.get();
-        throw new IOException("Cannot create repository " + name, err);
+      } catch (RepositoryNotFoundException notFound) {
+        try (Repository git = mgr.createRepository(allUsersName)) {
+          initAllUsers(git);
+          RefUpdate u = git.updateRef(Constants.HEAD);
+          u.link(RefNames.REFS_CONFIG);
+        } catch (RepositoryNotFoundException err) {
+          String name = allUsersName.get();
+          throw new IOException("Cannot create repository " + name, err);
+        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
index 0e22af9..57ec7ef 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.schema;
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.OFFLINE_OPERATION;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
@@ -26,6 +27,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.stream.IntStream;
@@ -87,15 +89,16 @@
       // seeded refs/meta/version during AllProjectsCreator, so it won't hit this block.
       checkNoteDbConfigFor216();
     }
-
-    for (int nextVersion : requiredUpgrades(currentVersion, schemaVersions.keySet())) {
-      try {
-        ui.message(String.format("Migrating data to schema %d ...", nextVersion));
-        NoteDbSchemaVersions.get(schemaVersions, nextVersion).upgrade(args, ui);
-        versionManager.increment(nextVersion - 1);
-      } catch (Exception e) {
-        throw new StorageException(
-            String.format("Failed to upgrade to schema version %d", nextVersion), e);
+    try (RefUpdateContext ctx = RefUpdateContext.open(OFFLINE_OPERATION)) {
+      for (int nextVersion : requiredUpgrades(currentVersion, schemaVersions.keySet())) {
+        try {
+          ui.message(String.format("Migrating data to schema %d ...", nextVersion));
+          NoteDbSchemaVersions.get(schemaVersions, nextVersion).upgrade(args, ui);
+          versionManager.increment(nextVersion - 1);
+        } catch (Exception e) {
+          throw new StorageException(
+              String.format("Failed to upgrade to schema version %d", nextVersion), e);
+        }
       }
     }
   }
diff --git a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
index 26ae4a8..38e45ab 100644
--- a/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
+++ b/java/com/google/gerrit/server/schema/SchemaCreatorImpl.java
@@ -38,6 +38,8 @@
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
 import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.server.update.context.RefUpdateContext;
+import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
 import com.google.inject.Inject;
 import java.io.IOException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -91,31 +93,33 @@
 
   @Override
   public void create() throws IOException, ConfigInvalidException {
-    GroupReference admins = createGroupReference("Administrators");
-    GroupReference serviceUsers = createGroupReference(ServiceUserClassifier.SERVICE_USERS);
+    try (RefUpdateContext ctx = RefUpdateContext.open(RefUpdateType.INIT_REPO)) {
+      GroupReference admins = createGroupReference("Administrators");
+      GroupReference serviceUsers = createGroupReference(ServiceUserClassifier.SERVICE_USERS);
 
-    AllProjectsInput allProjectsInput =
-        AllProjectsInput.builder()
-            .administratorsGroup(admins)
-            .serviceUsersGroup(serviceUsers)
-            .build();
-    allProjectsCreator.create(allProjectsInput);
-    // We have to create the All-Users repository before we can use it to store the groups in it.
-    allUsersCreator.setAdministrators(admins).create();
+      AllProjectsInput allProjectsInput =
+          AllProjectsInput.builder()
+              .administratorsGroup(admins)
+              .serviceUsersGroup(serviceUsers)
+              .build();
+      allProjectsCreator.create(allProjectsInput);
+      // We have to create the All-Users repository before we can use it to store the groups in it.
+      allUsersCreator.setAdministrators(admins).create();
 
-    // Don't rely on injection to construct Sequences, as the default GitReferenceUpdated has a
-    // thick dependency stack which may not all be available at schema creation time.
-    Sequences seqs =
-        new Sequences(
-            config,
-            repoManager,
-            GitReferenceUpdated.DISABLED,
-            allProjectsName,
-            allUsersName,
-            metricMaker);
-    try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
-      createAdminsGroup(seqs, allUsersRepo, admins);
-      createBatchUsersGroup(seqs, allUsersRepo, serviceUsers, admins.getUUID());
+      // Don't rely on injection to construct Sequences, as the default GitReferenceUpdated has a
+      // thick dependency stack which may not all be available at schema creation time.
+      Sequences seqs =
+          new Sequences(
+              config,
+              repoManager,
+              GitReferenceUpdated.DISABLED,
+              allProjectsName,
+              allUsersName,
+              metricMaker);
+      try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+        createAdminsGroup(seqs, allUsersRepo, admins);
+        createBatchUsersGroup(seqs, allUsersRepo, serviceUsers, admins.getUUID());
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/server/schema/Schema_184.java b/java/com/google/gerrit/server/schema/Schema_184.java
index 436c57b..a7e9506 100644
--- a/java/com/google/gerrit/server/schema/Schema_184.java
+++ b/java/com/google/gerrit/server/schema/Schema_184.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.schema;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.OFFLINE_OPERATION;
+
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupReference;
@@ -29,6 +31,7 @@
 import com.google.gerrit.server.group.db.GroupNameNotes;
 import com.google.gerrit.server.index.group.GroupIndex;
 import com.google.gerrit.server.index.group.GroupIndexCollection;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import java.io.IOException;
 import java.util.Optional;
 import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -84,17 +87,19 @@
       GroupConfig groupConfig,
       GroupNameNotes groupNameNotes)
       throws IOException {
-    BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
-    try (MetaDataUpdate metaDataUpdate =
-        createMetaDataUpdate(allUsersName, serverUser, allUsersRepo, batchRefUpdate)) {
-      groupConfig.commit(metaDataUpdate);
+    try (RefUpdateContext ctx = RefUpdateContext.open(OFFLINE_OPERATION)) {
+      BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
+      try (MetaDataUpdate metaDataUpdate =
+          createMetaDataUpdate(allUsersName, serverUser, allUsersRepo, batchRefUpdate)) {
+        groupConfig.commit(metaDataUpdate);
+      }
+      // MetaDataUpdates unfortunately can't be reused. -> Create a new one.
+      try (MetaDataUpdate metaDataUpdate =
+          createMetaDataUpdate(allUsersName, serverUser, allUsersRepo, batchRefUpdate)) {
+        groupNameNotes.commit(metaDataUpdate);
+      }
+      RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
     }
-    // MetaDataUpdates unfortunately can't be reused. -> Create a new one.
-    try (MetaDataUpdate metaDataUpdate =
-        createMetaDataUpdate(allUsersName, serverUser, allUsersRepo, batchRefUpdate)) {
-      groupNameNotes.commit(metaDataUpdate);
-    }
-    RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
   }
 
   private MetaDataUpdate createMetaDataUpdate(
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 14a636f..1d3ec73 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.MERGE_CHANGE;
 import static java.util.Comparator.comparing;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.toSet;
@@ -84,6 +85,7 @@
 import com.google.gerrit.server.update.SubmissionListener;
 import com.google.gerrit.server.update.SuperprojectUpdateOnSubmission;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -605,95 +607,98 @@
   private void integrateIntoHistory(
       ChangeSet cs, SubmissionExecutor submissionExecutor, boolean checkSubmitRules)
       throws RestApiException, UpdateException {
-    checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history");
-    logger.atFine().log("Beginning merge attempt on %s", cs);
-    Map<BranchNameKey, BranchBatch> toSubmit = new HashMap<>();
+    try (RefUpdateContext ctx = RefUpdateContext.open(MERGE_CHANGE)) {
+      checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history");
+      logger.atFine().log("Beginning merge attempt on %s", cs);
+      Map<BranchNameKey, BranchBatch> toSubmit = new HashMap<>();
 
-    ListMultimap<BranchNameKey, ChangeData> cbb;
-    try {
-      cbb = cs.changesByBranch();
-    } catch (StorageException e) {
-      throw new StorageException("Error reading changes to submit", e);
-    }
-    Set<BranchNameKey> branches = cbb.keySet();
-
-    for (BranchNameKey branch : branches) {
-      OpenRepo or = openRepo(branch.project());
-      if (or != null) {
-        toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
-      }
-    }
-
-    // Done checks that don't involve running submit strategies.
-    commitStatus.maybeFailVerbose();
-
-    try {
-      SubscriptionGraph subscriptionGraph = subscriptionGraphFactory.compute(branches, orm);
-      SubmoduleCommits submoduleCommits = submoduleCommitsFactory.create(orm);
-      UpdateOrderCalculator updateOrderCalculator = new UpdateOrderCalculator(subscriptionGraph);
-      List<SubmitStrategy> strategies =
-          getSubmitStrategies(
-              toSubmit, updateOrderCalculator, submoduleCommits, subscriptionGraph, dryrun);
-      this.projects = updateOrderCalculator.getProjectsInOrder();
-      List<BatchUpdate> batchUpdates =
-          orm.batchUpdates(
-              projects, /* refLogMessage= */ checkSubmitRules ? "merged" : "forced-merge");
-      // Group batch updates by project
-      Map<Project.NameKey, BatchUpdate> batchUpdatesByProject =
-          batchUpdates.stream().collect(Collectors.toMap(b -> b.getProject(), Function.identity()));
-      for (Map.Entry<Change.Id, ChangeData> entry : cs.changesById().entrySet()) {
-        Project.NameKey project = entry.getValue().project();
-        Change.Id changeId = entry.getKey();
-        ChangeData cd = entry.getValue();
-        batchUpdatesByProject
-            .get(project)
-            .addOp(
-                changeId,
-                storeSubmitRequirementsOpFactory.create(
-                    cd.submitRequirementsIncludingLegacy().values(), cd));
-      }
+      ListMultimap<BranchNameKey, ChangeData> cbb;
       try {
-        submissionExecutor.setAdditionalBatchUpdateListeners(
-            ImmutableList.of(new SubmitStrategyListener(submitInput, strategies, commitStatus)));
-        submissionExecutor.execute(batchUpdates);
-      } finally {
-        // If the BatchUpdate fails it can be that merging some of the changes was actually
-        // successful. This is why we must to collect the updated changes also when an
-        // exception was thrown.
-        strategies.forEach(s -> updatedChanges.putAll(s.getUpdatedChanges()));
+        cbb = cs.changesByBranch();
+      } catch (StorageException e) {
+        throw new StorageException("Error reading changes to submit", e);
+      }
+      Set<BranchNameKey> branches = cbb.keySet();
 
-        // Do not leave executed BatchUpdates in the OpenRepos
-        if (!dryrun) {
-          orm.resetUpdates(ImmutableSet.copyOf(this.projects));
+      for (BranchNameKey branch : branches) {
+        OpenRepo or = openRepo(branch.project());
+        if (or != null) {
+          toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
         }
       }
-    } catch (NoSuchProjectException e) {
-      throw new ResourceNotFoundException(e.getMessage());
-    } catch (IOException e) {
-      throw new StorageException(e);
-    } catch (SubmoduleConflictException e) {
-      throw new IntegrationConflictException(e.getMessage(), e);
-    } catch (UpdateException e) {
-      if (e.getCause() instanceof LockFailureException) {
-        // Lock failures are a special case: RetryHelper depends on this specific causal chain in
-        // order to trigger a retry. The downside of throwing here is we will not get the nicer
-        // error message constructed below, in the case where this is the final attempt and the
-        // operation is not retried further. This is not a huge downside, and is hopefully so rare
-        // as to be unnoticeable, assuming RetryHelper is retrying sufficiently.
-        throw e;
-      }
 
-      // BatchUpdate may have inadvertently wrapped an IntegrationConflictException
-      // thrown by some legacy SubmitStrategyOp code that intended the error
-      // message to be user-visible. Copy the message from the wrapped
-      // exception.
-      //
-      // If you happen across one of these, the correct fix is to convert the
-      // inner IntegrationConflictException to a ResourceConflictException.
-      if (e.getCause() instanceof IntegrationConflictException) {
-        throw (IntegrationConflictException) e.getCause();
+      // Done checks that don't involve running submit strategies.
+      commitStatus.maybeFailVerbose();
+
+      try {
+        SubscriptionGraph subscriptionGraph = subscriptionGraphFactory.compute(branches, orm);
+        SubmoduleCommits submoduleCommits = submoduleCommitsFactory.create(orm);
+        UpdateOrderCalculator updateOrderCalculator = new UpdateOrderCalculator(subscriptionGraph);
+        List<SubmitStrategy> strategies =
+            getSubmitStrategies(
+                toSubmit, updateOrderCalculator, submoduleCommits, subscriptionGraph, dryrun);
+        this.projects = updateOrderCalculator.getProjectsInOrder();
+        List<BatchUpdate> batchUpdates =
+            orm.batchUpdates(
+                projects, /* refLogMessage= */ checkSubmitRules ? "merged" : "forced-merge");
+        // Group batch updates by project
+        Map<Project.NameKey, BatchUpdate> batchUpdatesByProject =
+            batchUpdates.stream()
+                .collect(Collectors.toMap(b -> b.getProject(), Function.identity()));
+        for (Map.Entry<Change.Id, ChangeData> entry : cs.changesById().entrySet()) {
+          Project.NameKey project = entry.getValue().project();
+          Change.Id changeId = entry.getKey();
+          ChangeData cd = entry.getValue();
+          batchUpdatesByProject
+              .get(project)
+              .addOp(
+                  changeId,
+                  storeSubmitRequirementsOpFactory.create(
+                      cd.submitRequirementsIncludingLegacy().values(), cd));
+        }
+        try {
+          submissionExecutor.setAdditionalBatchUpdateListeners(
+              ImmutableList.of(new SubmitStrategyListener(submitInput, strategies, commitStatus)));
+          submissionExecutor.execute(batchUpdates);
+        } finally {
+          // If the BatchUpdate fails it can be that merging some of the changes was actually
+          // successful. This is why we must to collect the updated changes also when an
+          // exception was thrown.
+          strategies.forEach(s -> updatedChanges.putAll(s.getUpdatedChanges()));
+
+          // Do not leave executed BatchUpdates in the OpenRepos
+          if (!dryrun) {
+            orm.resetUpdates(ImmutableSet.copyOf(this.projects));
+          }
+        }
+      } catch (NoSuchProjectException e) {
+        throw new ResourceNotFoundException(e.getMessage());
+      } catch (IOException e) {
+        throw new StorageException(e);
+      } catch (SubmoduleConflictException e) {
+        throw new IntegrationConflictException(e.getMessage(), e);
+      } catch (UpdateException e) {
+        if (e.getCause() instanceof LockFailureException) {
+          // Lock failures are a special case: RetryHelper depends on this specific causal chain in
+          // order to trigger a retry. The downside of throwing here is we will not get the nicer
+          // error message constructed below, in the case where this is the final attempt and the
+          // operation is not retried further. This is not a huge downside, and is hopefully so rare
+          // as to be unnoticeable, assuming RetryHelper is retrying sufficiently.
+          throw e;
+        }
+
+        // BatchUpdate may have inadvertently wrapped an IntegrationConflictException
+        // thrown by some legacy SubmitStrategyOp code that intended the error
+        // message to be user-visible. Copy the message from the wrapped
+        // exception.
+        //
+        // If you happen across one of these, the correct fix is to convert the
+        // inner IntegrationConflictException to a ResourceConflictException.
+        if (e.getCause() instanceof IntegrationConflictException) {
+          throw (IntegrationConflictException) e.getCause();
+        }
+        throw new MergeUpdateException(genericMergeError(cs), e);
       }
-      throw new MergeUpdateException(genericMergeError(cs), e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/submit/SubmoduleOp.java b/java/com/google/gerrit/server/submit/SubmoduleOp.java
index ba736fa..cebb5e3 100644
--- a/java/com/google/gerrit/server/submit/SubmoduleOp.java
+++ b/java/com/google/gerrit/server/submit/SubmoduleOp.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.submit;
 
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.UPDATE_SUPERPROJECT;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.BranchNameKey;
@@ -25,6 +27,7 @@
 import com.google.gerrit.server.submit.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -103,10 +106,12 @@
           }
         }
       }
-      BatchUpdate.execute(
-          orm.batchUpdates(superProjects, /* refLogMessage= */ "merged"),
-          ImmutableList.of(),
-          dryrun);
+      try (RefUpdateContext ctx = RefUpdateContext.open(UPDATE_SUPERPROJECT)) {
+        BatchUpdate.execute(
+            orm.batchUpdates(superProjects, /* refLogMessage= */ "merged"),
+            ImmutableList.of(),
+            dryrun);
+      }
     } catch (UpdateException | IOException | NoSuchProjectException e) {
       throw new StorageException("Cannot update gitlinks", e);
     }
diff --git a/java/com/google/gerrit/server/update/context/RefUpdateContext.java b/java/com/google/gerrit/server/update/context/RefUpdateContext.java
new file mode 100644
index 0000000..1144e4f
--- /dev/null
+++ b/java/com/google/gerrit/server/update/context/RefUpdateContext.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2023 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.context;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * Passes additional information about an operation to the {@code BatchRefUpdate#execute} method.
+ *
+ * <p>To pass the additional information {@link RefUpdateContext}, wraps a code into an open
+ * RefUpdateContext, e.g.:
+ *
+ * <pre>{@code
+ * try(RefUpdateContext ctx = RefUpdateContext.open(RefUpdateType.CHANGE_MODIFICATION)) {
+ *   ...
+ *   // some code which modifies a ref using BatchRefUpdate.execute method
+ * }
+ * }</pre>
+ *
+ * When the {@code BatchRefUpdate#execute} method is executed, it can get all opened contexts and
+ * use it for an additional actions, e.g. it can put it in the reflog.
+ *
+ * <p>The information provided by this class is used internally in google.
+ *
+ * <p>The InMemoryRepositoryManager file makes some validation to ensure that RefUpdateContext is
+ * used correctly within the code (see thee validateRefUpdateContext method).
+ *
+ * <p>The class includes only operations from open-source gerrit and can be extended (see {@code
+ * TestActionRefUpdateContext} for example how to extend it).
+ */
+public class RefUpdateContext implements AutoCloseable {
+  private static final ThreadLocal<Deque<RefUpdateContext>> current = new ThreadLocal<>();
+
+  /**
+   * List of possible ref-update types.
+   *
+   * <p>Items in this enum are not fine-grained; different actions are shared the same type (e.g.
+   * {@link #CHANGE_MODIFICATION} includes posting comments, change edits and attention set update).
+   *
+   * <p>It is expected, that each type of operation should include only specific ref(s); check the
+   * validateRefUpdateContext in InMemoryRepositoryManager for relation between RefUpdateType and
+   * ref name.
+   */
+  public enum RefUpdateType {
+    /**
+     * Indicates that the context is implemented as a descendant of the {@link RefUpdateContext} .
+     *
+     * <p>The {@link #getUpdateType()} returns this type for all descendant of {@link
+     * RefUpdateContext}. This type is never returned if the context is exactly {@link
+     * RefUpdateContext}.
+     */
+    OTHER,
+    /**
+     * A ref is updated as a part of change-related operation.
+     *
+     * <p>This covers multiple different cases - creating and uploading changes and patchsets,
+     * comments operations, change edits, etc...
+     */
+    CHANGE_MODIFICATION,
+    /** A ref is updated during merge-change operation. */
+    MERGE_CHANGE,
+    /** A ref is updated as a part of a repo sequence operation. */
+    REPO_SEQ,
+    /** A ref is updated as a part of a repo initialization. */
+    INIT_REPO,
+    /** A ref is udpated as a part of gpg keys modification. */
+    GPG_KEYS_MODIFICATION,
+    /** A ref is updated as a part of group(s) update */
+    GROUPS_UPDATE,
+    /** A ref is updated as a part of account(s) update. */
+    ACCOUNTS_UPDATE,
+    /** A ref is updated as a part of direct push. */
+    DIRECT_PUSH,
+    /** A ref is updated as a part of explicit branch or ref update operation. */
+    BRANCH_MODIFICATION,
+    /** A ref is updated as a part of explicit tag update operation. */
+    TAG_MODIFICATION,
+    /**
+     * A tag is updated as a part of an offline operation.
+     *
+     * <p>Offline operation - an operation which is executed separately from the gerrit server and
+     * can't be triggered by any gerrit API. E.g. schema update.
+     */
+    OFFLINE_OPERATION,
+    /** A tag is updated as a part of an update-superproject flow. */
+    UPDATE_SUPERPROJECT,
+    /** A ref is updated as a part of explicit HEAD update operation. */
+    HEAD_MODIFICATION,
+    /** A ref is updated as a part of versioned meta data change. */
+    VERSIONED_META_DATA_CHANGE,
+    /** A ref is updated as a part of commit-ban operation. */
+    BAN_COMMIT,
+    /**
+     * A ref is updated inside a plugin.
+     *
+     * <p>If a plugin updates one of a special refs - it must also open a nested context.
+     */
+    PLUGIN,
+  }
+
+  /** Opens a provided context. */
+  protected static <T extends RefUpdateContext> T open(T ctx) {
+    getCurrent().addLast(ctx);
+    return ctx;
+  }
+
+  /** Opens a context of a give type. */
+  public static RefUpdateContext open(RefUpdateType updateType) {
+    checkArgument(updateType != RefUpdateType.OTHER, "The OTHER type is for internal use only.");
+    return open(new RefUpdateContext(updateType));
+  }
+
+  /** Returns the list of opened contexts; the first element is the outermost context. */
+  public static ImmutableList<RefUpdateContext> getOpenedContexts() {
+    return ImmutableList.copyOf(getCurrent());
+  }
+
+  /** Checks if there is an open context of the given type. */
+  public static boolean hasOpen(RefUpdateType type) {
+    return getCurrent().stream().anyMatch(ctx -> ctx.getUpdateType() == type);
+  }
+
+  private final RefUpdateType updateType;
+
+  private RefUpdateContext(RefUpdateType updateType) {
+    this.updateType = updateType;
+  }
+
+  protected RefUpdateContext() {
+    this(RefUpdateType.OTHER);
+  }
+
+  protected static final Deque<RefUpdateContext> getCurrent() {
+    Deque<RefUpdateContext> result = current.get();
+    if (result == null) {
+      result = new ArrayDeque<>();
+      current.set(result);
+    }
+    return result;
+  }
+
+  /**
+   * Returns the type of {@link RefUpdateContext}.
+   *
+   * <p>For descendants, always return {@link RefUpdateType#OTHER}
+   */
+  public final RefUpdateType getUpdateType() {
+    return updateType;
+  }
+
+  /** Closes the current context. */
+  @Override
+  public void close() {
+    Deque<RefUpdateContext> openedContexts = getCurrent();
+    checkState(
+        openedContexts.peekLast() == this, "The current context is different from this context.");
+    openedContexts.removeLast();
+  }
+}
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index e5234fe..fb9e64e 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -5,7 +5,10 @@
     testonly = True,
     srcs = glob(
         ["**/*.java"],
-        exclude = ["AssertableExecutorService.java"],
+        exclude = [
+            "AssertableExecutorService.java",
+            "TestActionRefUpdateContext.java",
+        ],
     ),
     visibility = ["//visibility:public"],
     exports = [
@@ -40,6 +43,7 @@
         "//java/com/google/gerrit/server/restapi",
         "//java/com/google/gerrit/server/schema",
         "//java/com/google/gerrit/server/util/time",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//lib:guava",
         "//lib:h2",
         "//lib:jgit",
@@ -66,3 +70,14 @@
         "//lib/truth",
     ],
 )
+
+java_library(
+    name = "test-ref-update-context",
+    testonly = True,
+    srcs = ["TestActionRefUpdateContext.java"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//java/com/google/gerrit/server",
+        "//lib/errorprone:annotations",
+    ],
+)
diff --git a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
index 2051ae3..8d1130c 100644
--- a/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
+++ b/java/com/google/gerrit/testing/InMemoryRepositoryManager.java
@@ -14,20 +14,56 @@
 
 package com.google.gerrit.testing;
 
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.ACCOUNTS_UPDATE;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BAN_COMMIT;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.BRANCH_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.DIRECT_PUSH;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GPG_KEYS_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GROUPS_UPDATE;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.HEAD_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.MERGE_CHANGE;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.OFFLINE_OPERATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.PLUGIN;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.REPO_SEQ;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.TAG_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.VERSIONED_META_DATA_CHANGE;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import com.google.gerrit.server.update.context.RefUpdateContext;
+import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
 import com.google.inject.Inject;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.NavigableSet;
+import java.util.Optional;
+import java.util.function.Predicate;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase;
+import org.eclipse.jgit.internal.storage.dfs.DfsReftableBatchRefUpdate;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 
 /** Repository manager that uses in-memory repositories. */
 public class InMemoryRepositoryManager implements GitRepositoryManager {
@@ -56,6 +92,137 @@
       setPerformsAtomicTransactions(true);
     }
 
+    /** Validates that a given ref is updated within the expected context. */
+    private static class RefUpdateContextValidator {
+      /**
+       * A configured singleton for ref context validation.
+       *
+       * <p>Each ref must match no more than 1 special ref from the list below. If ref is not
+       * matched to any special ref predicate, then it is checked against the standard rules - check
+       * the code of the {@link #validateRefUpdateContext} for details.
+       */
+      public static final RefUpdateContextValidator INSTANCE =
+          new RefUpdateContextValidator()
+              .addSpecialRef(RefNames::isSequenceRef, REPO_SEQ)
+              .addSpecialRef(RefNames.HEAD::equals, HEAD_MODIFICATION)
+              .addSpecialRef(RefNames::isRefsChanges, CHANGE_MODIFICATION, MERGE_CHANGE)
+              .addSpecialRef(RefNames::isAutoMergeRef, CHANGE_MODIFICATION)
+              .addSpecialRef(RefNames::isRefsEdit, CHANGE_MODIFICATION, MERGE_CHANGE)
+              .addSpecialRef(RefNames::isTagRef, TAG_MODIFICATION)
+              .addSpecialRef(RefNames::isRejectCommitsRef, BAN_COMMIT)
+              .addSpecialRef(
+                  name -> RefNames.isRefsUsers(name) && !RefNames.isRefsEdit(name),
+                  VERSIONED_META_DATA_CHANGE,
+                  ACCOUNTS_UPDATE,
+                  MERGE_CHANGE)
+              .addSpecialRef(
+                  RefNames::isConfigRef,
+                  VERSIONED_META_DATA_CHANGE,
+                  BRANCH_MODIFICATION,
+                  MERGE_CHANGE)
+              .addSpecialRef(RefNames::isExternalIdRef, VERSIONED_META_DATA_CHANGE, ACCOUNTS_UPDATE)
+              .addSpecialRef(PublicKeyStore.REFS_GPG_KEYS::equals, GPG_KEYS_MODIFICATION)
+              .addSpecialRef(RefNames::isRefsDraftsComments, CHANGE_MODIFICATION)
+              .addSpecialRef(RefNames::isRefsStarredChanges, CHANGE_MODIFICATION)
+              // A user can create a change for updating a group and then merge it.
+              // The GroupsIT#pushToGroupBranchForReviewForNonAllUsersRepoAndSubmit test verifies
+              // this scenario.
+              .addSpecialRef(RefNames::isGroupRef, GROUPS_UPDATE, MERGE_CHANGE);
+
+      private List<Entry<Predicate<String>, ImmutableList<RefUpdateType>>> specialRefs =
+          new ArrayList<>();
+
+      private RefUpdateContextValidator() {}
+
+      public void validateRefUpdateContext(ReceiveCommand cmd) {
+        if (TestActionRefUpdateContext.isOpen()
+            || RefUpdateContext.hasOpen(OFFLINE_OPERATION)
+            || RefUpdateContext.hasOpen(INIT_REPO)
+            || RefUpdateContext.hasOpen(DIRECT_PUSH)) {
+          // The action can touch any refs in these contexts.
+          return;
+        }
+
+        String refName = cmd.getRefName();
+
+        Optional<ImmutableList<RefUpdateType>> allowedRefUpdateTypes =
+            RefUpdateContextValidator.INSTANCE.getAllowedRefUpdateTypes(refName);
+
+        if (allowedRefUpdateTypes.isPresent()) {
+          checkState(
+              allowedRefUpdateTypes.get().stream().anyMatch(RefUpdateContext::hasOpen)
+                  || isTestRepoCall(),
+              "Special ref '%s' is updated outside of the expected operation. Wrap code in the correct RefUpdateContext or fix allowed update types",
+              refName);
+          return;
+        }
+        // It is not one of the special ref - update is possible only within specific contexts.
+        checkState(
+            RefUpdateContext.hasOpen(MERGE_CHANGE)
+                || RefUpdateContext.hasOpen(RefUpdateType.BRANCH_MODIFICATION)
+                || RefUpdateContext.hasOpen(RefUpdateType.UPDATE_SUPERPROJECT)
+                // Plugin can update any ref
+                || RefUpdateContext.hasOpen(PLUGIN)
+                || isTestRepoCall(),
+            "Ordinary ref '%s' is updated outside of the expected operation. Wrap code in the correct RefUpdateContext or add the ref as a special ref.",
+            refName);
+      }
+
+      private RefUpdateContextValidator addSpecialRef(
+          Predicate<String> refNamePredicate, RefUpdateType... validRefUpdateTypes) {
+        specialRefs.add(
+            new SimpleImmutableEntry<>(
+                refNamePredicate, ImmutableList.copyOf(validRefUpdateTypes)));
+        return this;
+      }
+
+      private Optional<ImmutableList<RefUpdateType>> getAllowedRefUpdateTypes(String refName) {
+        List<ImmutableList<RefUpdateType>> allowedTypes =
+            specialRefs.stream()
+                .filter(entry -> entry.getKey().test(refName))
+                .map(Entry::getValue)
+                .collect(toList());
+        checkState(
+            allowedTypes.size() <= 1,
+            "refName matches more than 1 predicate. Please fix the specialRefs list, so each reference has no more than one match.");
+        if (allowedTypes.size() == 0) {
+          return Optional.empty();
+        }
+        return Optional.of(allowedTypes.get(0));
+      }
+
+      /**
+       * Returns true if a ref is updated using one of the method in {@link
+       * org.eclipse.jgit.junit.TestRepository}.
+       *
+       * <p>The {@link org.eclipse.jgit.junit.TestRepository} used only in tests and allows to
+       * change refs directly. Wrapping each usage in a test context requires a lot of modification,
+       * so instead we allow any ref updates, which are made using through this class.
+       */
+      private boolean isTestRepoCall() {
+        return Arrays.stream(Thread.currentThread().getStackTrace())
+            .anyMatch(elem -> elem.getClassName().equals("org.eclipse.jgit.junit.TestRepository"));
+      }
+    }
+
+    @Override
+    protected MemRefDatabase createRefDatabase() {
+      return new MemRefDatabase() {
+        @Override
+        public BatchRefUpdate newBatchUpdate() {
+          DfsObjDatabase odb = getRepository().getObjectDatabase();
+          return new DfsReftableBatchRefUpdate(this, odb) {
+            @Override
+            public void execute(RevWalk rw, ProgressMonitor pm, List<String> options) {
+              getCommands().stream()
+                  .forEach(RefUpdateContextValidator.INSTANCE::validateRefUpdateContext);
+              super.execute(rw, pm, options);
+            }
+          };
+        }
+      };
+    }
+
     @Override
     public Description getDescription() {
       return (Description) super.getDescription();
diff --git a/java/com/google/gerrit/testing/TestActionRefUpdateContext.java b/java/com/google/gerrit/testing/TestActionRefUpdateContext.java
new file mode 100644
index 0000000..23ec9aa
--- /dev/null
+++ b/java/com/google/gerrit/testing/TestActionRefUpdateContext.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2023 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.testing;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.server.update.context.RefUpdateContext;
+
+/**
+ * Marks ref updates as a test actions.
+ *
+ * <p>This class should be used in tests only to wrap a portion of test code which directly modifies
+ * references. Usage:
+ *
+ * <pre>{@code
+ * import static ...TestActionRefUpdateContext.openTestRefUpdateContext();
+ *
+ * try(RefUpdateContext ctx=openTestRefUpdateContext()) {
+ *   // Some test code, which modifies a reference.
+ * }
+ * }</pre>
+ *
+ * or
+ *
+ * <pre>{@code
+ * import static ...TestActionRefUpdateContext.testRefAction;
+ *
+ * testRefAction(() -> {doSomethingWithRef()});
+ * T result = testRefAction(() -> { return doSomethingWithRef()});
+ * }</pre>
+ */
+public final class TestActionRefUpdateContext extends RefUpdateContext {
+  public static boolean isOpen() {
+    return getCurrent().stream().anyMatch(ctx -> ctx instanceof TestActionRefUpdateContext);
+  }
+
+  public static TestActionRefUpdateContext openTestRefUpdateContext() {
+    return open(new TestActionRefUpdateContext());
+  }
+
+  @CanIgnoreReturnValue
+  public static <V, E extends Exception> V testRefAction(CallableWithException<V, E> c) throws E {
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      return c.call();
+    }
+  }
+
+  public static <E extends Exception> void testRefAction(RunnableWithException<E> c) throws E {
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      c.run();
+    }
+  }
+
+  public interface CallableWithException<V, E extends Exception> {
+    V call() throws E;
+  }
+
+  @FunctionalInterface
+  public interface RunnableWithException<E extends Exception> {
+    void run() throws E;
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index b6e5b74..33e6692 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.only;
 import static org.mockito.Mockito.verify;
@@ -32,6 +33,7 @@
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.index.group.GroupIndexer;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.TestTimeUtil;
@@ -286,14 +288,16 @@
   }
 
   private Ref createRef(Repository repo, String ref) throws IOException {
-    try (ObjectInserter oi = repo.newObjectInserter();
-        RevWalk rw = new RevWalk(repo)) {
-      ObjectId emptyCommit = createCommit(repo);
-      RefUpdate updateRef = repo.updateRef(ref);
-      updateRef.setExpectedOldObjectId(ObjectId.zeroId());
-      updateRef.setNewObjectId(emptyCommit);
-      assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.NEW);
-      return repo.exactRef(ref);
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (ObjectInserter oi = repo.newObjectInserter();
+          RevWalk rw = new RevWalk(repo)) {
+        ObjectId emptyCommit = createCommit(repo);
+        RefUpdate updateRef = repo.updateRef(ref);
+        updateRef.setExpectedOldObjectId(ObjectId.zeroId());
+        updateRef.setNewObjectId(emptyCommit);
+        assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.NEW);
+        return repo.exactRef(ref);
+      }
     }
   }
 
@@ -302,17 +306,19 @@
   }
 
   private Ref updateRef(Repository repo, Ref ref) throws IOException {
-    try (ObjectInserter oi = repo.newObjectInserter();
-        RevWalk rw = new RevWalk(repo)) {
-      ObjectId emptyCommit = createCommit(repo);
-      RefUpdate updateRef = repo.updateRef(ref.getName());
-      updateRef.setExpectedOldObjectId(ref.getObjectId());
-      updateRef.setNewObjectId(emptyCommit);
-      updateRef.setForceUpdate(true);
-      assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.FORCED);
-      Ref updatedRef = repo.exactRef(ref.getName());
-      assertThat(updatedRef.getObjectId()).isNotEqualTo(ref.getObjectId());
-      return updatedRef;
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (ObjectInserter oi = repo.newObjectInserter();
+          RevWalk rw = new RevWalk(repo)) {
+        ObjectId emptyCommit = createCommit(repo);
+        RefUpdate updateRef = repo.updateRef(ref.getName());
+        updateRef.setExpectedOldObjectId(ref.getObjectId());
+        updateRef.setNewObjectId(emptyCommit);
+        updateRef.setForceUpdate(true);
+        assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.FORCED);
+        Ref updatedRef = repo.exactRef(ref.getName());
+        assertThat(updatedRef.getObjectId()).isNotEqualTo(ref.getObjectId());
+        return updatedRef;
+      }
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 111f8c9..2c1739d 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -42,6 +42,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
@@ -264,7 +265,7 @@
       if (ref != null) {
         RefUpdate ru = repo.updateRef(REFS_GPG_KEYS);
         ru.setForceUpdate(true);
-        assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+        testRefAction(() -> assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED));
       }
     }
   }
@@ -1888,7 +1889,7 @@
 
       // Mark first key as invalid
       assertThat(info.get(0).valid).isTrue();
-      authorizedKeys.markKeyInvalid(admin.id(), 1);
+      testRefAction(() -> authorizedKeys.markKeyInvalid(admin.id(), 1));
       info = gApi.accounts().self().listSshKeys();
       assertThat(info).hasSize(2);
       assertThat(info.get(0).seq).isEqualTo(1);
@@ -2434,79 +2435,88 @@
 
     // Manually updating the user ref makes the index document stale.
     String userRef = RefNames.refsUsers(accountId);
-    try (Repository repo = repoManager.openRepository(allUsers);
-        ObjectInserter oi = repo.newObjectInserter();
-        RevWalk rw = new RevWalk(repo)) {
-      RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
+    testRefAction(
+        () -> {
+          try (Repository repo = repoManager.openRepository(allUsers);
+              ObjectInserter oi = repo.newObjectInserter();
+              RevWalk rw = new RevWalk(repo)) {
+            RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
 
-      PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.now());
-      CommitBuilder cb = new CommitBuilder();
-      cb.setTreeId(commit.getTree());
-      cb.setCommitter(ident);
-      cb.setAuthor(ident);
-      cb.setMessage(commit.getFullMessage());
-      ObjectId emptyCommit = oi.insert(cb);
-      oi.flush();
+            PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.now());
+            CommitBuilder cb = new CommitBuilder();
+            cb.setTreeId(commit.getTree());
+            cb.setCommitter(ident);
+            cb.setAuthor(ident);
+            cb.setMessage(commit.getFullMessage());
+            ObjectId emptyCommit = oi.insert(cb);
+            oi.flush();
 
-      RefUpdate updateRef = repo.updateRef(userRef);
-      updateRef.setExpectedOldObjectId(commit.toObjectId());
-      updateRef.setNewObjectId(emptyCommit);
-      assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
-    }
+            RefUpdate updateRef = repo.updateRef(userRef);
+            updateRef.setExpectedOldObjectId(commit.toObjectId());
+            updateRef.setNewObjectId(emptyCommit);
+            assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+          }
+        });
     assertStaleAccountAndReindex(accountId);
 
     // Manually inserting/updating/deleting an external ID of the user makes the index document
     // stale.
     try (Repository repo = repoManager.openRepository(allUsers)) {
-      ExternalIdNotes extIdNotes =
-          ExternalIdNotes.load(
-              allUsers,
-              repo,
-              externalIdFactory,
-              authConfig.isUserNameCaseInsensitiveMigrationMode());
+      testRefAction(
+          () -> {
+            ExternalIdNotes extIdNotes =
+                ExternalIdNotes.load(
+                    allUsers,
+                    repo,
+                    externalIdFactory,
+                    authConfig.isUserNameCaseInsensitiveMigrationMode());
 
-      ExternalId.Key key = externalIdKeyFactory.create("foo", "foo");
-      extIdNotes.insert(externalIdFactory.create(key, accountId));
-      try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
-        extIdNotes.commit(update);
-      }
-      assertStaleAccountAndReindex(accountId);
+            ExternalId.Key key = externalIdKeyFactory.create("foo", "foo");
+            extIdNotes.insert(externalIdFactory.create(key, accountId));
+            try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
+              extIdNotes.commit(update);
+            }
+            assertStaleAccountAndReindex(accountId);
 
-      extIdNotes =
-          ExternalIdNotes.load(
-              allUsers,
-              repo,
-              externalIdFactory,
-              authConfig.isUserNameCaseInsensitiveMigrationMode());
-      extIdNotes.upsert(externalIdFactory.createWithEmail(key, accountId, "foo@example.com"));
-      try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
-        extIdNotes.commit(update);
-      }
-      assertStaleAccountAndReindex(accountId);
+            extIdNotes =
+                ExternalIdNotes.load(
+                    allUsers,
+                    repo,
+                    externalIdFactory,
+                    authConfig.isUserNameCaseInsensitiveMigrationMode());
+            extIdNotes.upsert(externalIdFactory.createWithEmail(key, accountId, "foo@example.com"));
+            try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
+              extIdNotes.commit(update);
+            }
+            assertStaleAccountAndReindex(accountId);
 
-      extIdNotes =
-          ExternalIdNotes.load(
-              allUsers,
-              repo,
-              externalIdFactory,
-              authConfig.isUserNameCaseInsensitiveMigrationMode());
-      extIdNotes.delete(accountId, key);
-      try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
-        extIdNotes.commit(update);
-      }
+            extIdNotes =
+                ExternalIdNotes.load(
+                    allUsers,
+                    repo,
+                    externalIdFactory,
+                    authConfig.isUserNameCaseInsensitiveMigrationMode());
+            extIdNotes.delete(accountId, key);
+            try (MetaDataUpdate update = metaDataUpdateFactory.create(allUsers)) {
+              extIdNotes.commit(update);
+            }
+          });
       assertStaleAccountAndReindex(accountId);
     }
 
     // Manually delete account
-    try (Repository repo = repoManager.openRepository(allUsers);
-        RevWalk rw = new RevWalk(repo)) {
-      RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
-      RefUpdate updateRef = repo.updateRef(userRef);
-      updateRef.setExpectedOldObjectId(commit.toObjectId());
-      updateRef.setNewObjectId(ObjectId.zeroId());
-      updateRef.setForceUpdate(true);
-      assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
-    }
+    testRefAction(
+        () -> {
+          try (Repository repo = repoManager.openRepository(allUsers);
+              RevWalk rw = new RevWalk(repo)) {
+            RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
+            RefUpdate updateRef = repo.updateRef(userRef);
+            updateRef.setExpectedOldObjectId(commit.toObjectId());
+            updateRef.setNewObjectId(ObjectId.zeroId());
+            updateRef.setForceUpdate(true);
+            assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
+          }
+        });
     assertStaleAccountAndReindex(accountId);
   }
 
@@ -3385,16 +3395,19 @@
   }
 
   private Map<String, GpgKeyInfo> addGpgKey(TestAccount account, String armored) throws Exception {
-    AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
-    try (Registration registration =
-        extensionRegistry.newRegistration().add(accountIndexedCounter)) {
-      Map<String, GpgKeyInfo> gpgKeys =
-          gApi.accounts()
-              .id(account.username())
-              .putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
-      accountIndexedCounter.assertReindexOf(gApi.accounts().id(account.username()).get());
-      return gpgKeys;
-    }
+    return testRefAction(
+        () -> {
+          AccountIndexedCounter accountIndexedCounter = new AccountIndexedCounter();
+          try (Registration registration =
+              extensionRegistry.newRegistration().add(accountIndexedCounter)) {
+            Map<String, GpgKeyInfo> gpgKeys =
+                gApi.accounts()
+                    .id(account.username())
+                    .putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of());
+            accountIndexedCounter.assertReindexOf(gApi.accounts().id(account.username()).get());
+            return gpgKeys;
+          }
+        });
   }
 
   private Map<String, GpgKeyInfo> addGpgKeyNoReindex(String armored) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
index d1258fc..1693411 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIndexerIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -32,6 +33,7 @@
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.index.account.AccountIndexer;
 import com.google.gerrit.server.query.account.InternalAccountQuery;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -141,16 +143,19 @@
 
   private void updateAccountWithoutCacheOrIndex(Account.Id accountId, AccountDelta accountDelta)
       throws IOException, ConfigInvalidException {
-    try (Repository allUsersRepo = repoManager.openRepository(allUsersName);
-        MetaDataUpdate md =
-            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo)) {
-      PersonIdent ident = serverIdent.get();
-      md.getCommitBuilder().setAuthor(ident);
-      md.getCommitBuilder().setCommitter(ident);
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (Repository allUsersRepo = repoManager.openRepository(allUsersName);
+          MetaDataUpdate md =
+              new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo)) {
+        PersonIdent ident = serverIdent.get();
+        md.getCommitBuilder().setAuthor(ident);
+        md.getCommitBuilder().setCommitter(ident);
 
-      AccountConfig accountConfig = new AccountConfig(accountId, allUsersName, allUsersRepo).load();
-      accountConfig.setAccountDelta(accountDelta);
-      accountConfig.commit(md);
+        AccountConfig accountConfig =
+            new AccountConfig(accountId, allUsersName, allUsersRepo).load();
+        accountConfig.setAccountDelta(accountDelta);
+        accountConfig.commit(md);
+      }
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 7e23f0e..875b520 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.collect.ImmutableSet;
@@ -45,6 +46,7 @@
 import com.google.gerrit.server.group.db.GroupsUpdate;
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.inject.Inject;
 import com.google.inject.util.Providers;
 import java.util.Optional;
@@ -285,11 +287,13 @@
     // Create orphaned SCHEME_GERRIT external ID.
     Account.Id accountId = Account.id(seq.nextAccountId());
     ExternalId gerritExtId = externalIdFactory.create(gerritExtIdKey, accountId);
-    try (Repository allUsersRepo = repoManager.openRepository(allUsers);
-        MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
-      ExternalIdNotes extIdNotes = extIdNotesFactory.load(allUsersRepo);
-      extIdNotes.insert(gerritExtId);
-      extIdNotes.commit(md);
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+          MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+        ExternalIdNotes extIdNotes = extIdNotesFactory.load(allUsersRepo);
+        extIdNotes.insert(gerritExtId);
+        extIdNotes.commit(md);
+      }
     }
 
     AuthRequest who = authRequestFactory.createForUser(username);
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
index 3c605e1..c441402 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/BUILD
@@ -13,6 +13,7 @@
         "//java/com/google/gerrit/git",
         "//java/com/google/gerrit/mail",
         "//java/com/google/gerrit/server/util/time",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
     ],
 )
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index 898e1ff..0b55563 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -213,6 +213,29 @@
   }
 
   @Test
+  public void applyGerritBasedPatchUsingRestWithEncodedPatch_success() throws Exception {
+    String head = getHead(repo(), HEAD).name();
+    createBranchWithRevision(BranchNameKey.create(project, "branch"), head);
+    PushOneCommit.Result baseCommit = createChange("Add file", ADDED_FILE_NAME, ADDED_FILE_CONTENT);
+    baseCommit.assertOkStatus();
+    createBranchWithRevision(BranchNameKey.create(project, DESTINATION_BRANCH), head);
+    RestResponse patchResp =
+        userRestSession.get("/changes/" + baseCommit.getChangeId() + "/revisions/current/patch");
+    patchResp.assertOK();
+    String originalEncodedPatch = patchResp.getEntityContent();
+    String originalDecodedPatch = new String(Base64.decode(patchResp.getEntityContent()), UTF_8);
+    ApplyPatchPatchSetInput in = buildInput(originalEncodedPatch);
+    PushOneCommit.Result destChange = createChange();
+
+    RestResponse resp =
+        adminRestSession.post("/changes/" + destChange.getChangeId() + "/patch:apply", in);
+
+    resp.assertOK();
+    BinaryResult resultPatch = gApi.changes().id(destChange.getChangeId()).current().patch();
+    assertThat(removeHeader(resultPatch)).isEqualTo(removeHeader(originalDecodedPatch));
+  }
+
+  @Test
   public void applyPatchWithConflict_fails() throws Exception {
     initBaseWithFile(MODIFIED_FILE_NAME, "Unexpected base content");
     ApplyPatchPatchSetInput in = buildInput(MODIFIED_FILE_DIFF);
@@ -404,6 +427,6 @@
   }
 
   private String removeHeader(String s) {
-    return s.substring(s.indexOf("\ndiff --git"), s.length() - 1);
+    return s.substring(s.lastIndexOf("\ndiff --git"), s.length() - 1);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 215d1e8..e3d69e1 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -56,6 +56,7 @@
 import static com.google.gerrit.server.project.testing.TestLabels.label;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 import static com.google.gerrit.truth.CacheStatsSubject.assertThat;
 import static com.google.gerrit.truth.CacheStatsSubject.cloneStats;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -181,6 +182,7 @@
 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.RefUpdateContext;
 import com.google.gerrit.server.util.AccountTemplateUtil;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.FakeEmailSender.Message;
@@ -2908,9 +2910,11 @@
   @Test
   public void submitToSymref() throws Exception {
     // Create symref in the origin repository (testRepo references to a local repository)
-    try (Repository repo = repoManager.openRepository(project)) {
-      RefUpdate u = repo.updateRef("refs/heads/master_symref");
-      assertThat(u.link("refs/heads/master")).isEqualTo(Result.NEW);
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (Repository repo = repoManager.openRepository(project)) {
+        RefUpdate u = repo.updateRef("refs/heads/master_symref");
+        assertThat(u.link("refs/heads/master")).isEqualTo(Result.NEW);
+      }
     }
 
     PushOneCommit.Result r = createChange("refs/for/master_symref");
@@ -4231,10 +4235,12 @@
   }
 
   private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception {
-    try (BatchUpdate batchUpdate =
-        batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.now())) {
-      batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus));
-      batchUpdate.execute();
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (BatchUpdate batchUpdate =
+          batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.now())) {
+        batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus));
+        batchUpdate.execute();
+      }
     }
 
     ChangeStatus changeStatus = gApi.changes().id(id.get()).get().status;
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
index 267f5a7..519c1dc 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -252,20 +253,22 @@
     try (BatchUpdate u =
         batchUpdateFactory.create(
             project, identifiedUserFactory.create(admin.id()), TimeUtil.now())) {
-      u.addOp(
-              changeId,
-              new BatchUpdateOp() {
-                @Override
-                public boolean updateChange(ChangeContext ctx) {
-                  ctx.getChange().setPrivate(true);
-                  ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
-                  ctx.getChange().setPrivate(true);
-                  ctx.getChange().setLastUpdatedOn(ctx.getWhen());
-                  update.setPrivate(true);
-                  return true;
-                }
-              })
-          .execute();
+      testRefAction(
+          () ->
+              u.addOp(
+                      changeId,
+                      new BatchUpdateOp() {
+                        @Override
+                        public boolean updateChange(ChangeContext ctx) {
+                          ctx.getChange().setPrivate(true);
+                          ChangeUpdate update = ctx.getUpdate(ctx.getChange().currentPatchSetId());
+                          ctx.getChange().setPrivate(true);
+                          ctx.getChange().setLastUpdatedOn(ctx.getWhen());
+                          update.setPrivate(true);
+                          return true;
+                        }
+                      })
+                  .execute());
     }
     assertThat(gApi.changes().id(changeId.get()).get().isPrivate).isTrue();
   }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
index 56e23a4..2fe7038 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitRequirementPredicateIT.java
@@ -17,6 +17,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.testing.TestLabels.codeReview;
 import static com.google.gerrit.server.project.testing.TestLabels.label;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static org.eclipse.jgit.lib.Constants.HEAD;
@@ -26,6 +28,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.UseTimezone;
 import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
@@ -34,15 +38,21 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.entities.SubmitRequirementExpression;
 import com.google.gerrit.entities.SubmitRequirementExpressionResult;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.server.project.SubmitRequirementsEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.merge.MergeStrategy;
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -294,6 +304,162 @@
         "unexpected base value format");
   }
 
+  @Test
+  public void nonContributorLabelVote_match() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, user);
+    PushOneCommit.Result r1 =
+        pushFactory
+            .create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
+            .to("refs/for/master");
+
+    Change.Id cId = r1.getChange().getId();
+
+    ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
+
+    // Assert on uploader, committer and author
+    assertUploader(changeInfo, user.email());
+    assertCommitter(changeInfo, user.email());
+    assertAuthor(changeInfo, user.email());
+
+    // Vote from admin (a.k.a. non uploader/committer/author) matches
+    requestScopeOperations.setApiUser(admin.id());
+    approve(cId.toString());
+    assertMatching("label:Code-Review=+2,user=non_contributor", cId);
+    // Also make sure magic label votes and > operator work
+    assertMatching("label:Code-Review=MAX,user=non_contributor", cId);
+    assertMatching("label:Code-Review>+1,user=non_contributor", cId);
+  }
+
+  @Test
+  public void nonContributorLabelVote_voteFromUploader_doesNotMatch() throws Exception {
+    PushOneCommit.Result r1 = createNormalCommit(user.newIdent(), "refs/for/master", "file1");
+
+    ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
+    assertUploader(changeInfo, admin.email());
+
+    // Vote from admin (a.k.a. uploader) does not match
+    requestScopeOperations.setApiUser(admin.id());
+    approve(r1.getChangeId());
+    assertNotMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
+  }
+
+  @Test
+  @Sandboxed
+  public void nonContributorLabelVote_voteFromAuthor_doesNotMatch() throws Exception {
+    Account.Id authorId =
+        accountOperations
+            .newAccount()
+            .fullname("author")
+            .preferredEmail("authoremail@example.com")
+            .create();
+    Account.Id committerId =
+        accountOperations
+            .newAccount()
+            .fullname("committer")
+            .preferredEmail("committeremail@example.com")
+            .create();
+
+    Change.Id changeId =
+        changeOperations.newChange().author(authorId).committer(committerId).create();
+    ChangeInfo changeInfo = gApi.changes().id(changeId.get()).get();
+    assertAuthor(changeInfo, "authoremail@example.com");
+
+    allowLabelPermission(
+        codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
+
+    // Vote from author does not match
+    requestScopeOperations.setApiUser(authorId);
+    approve(changeId.toString());
+    assertNotMatching("label:Code-Review=+2,user=non_contributor", changeId);
+  }
+
+  @Test
+  public void nonContributorLabelVote_voteFromCommitter_doesNotMatch() throws Exception {
+    Account.Id authorId =
+        accountOperations
+            .newAccount()
+            .fullname("author")
+            .preferredEmail("authoremail@example.com")
+            .create();
+    Account.Id committerId =
+        accountOperations
+            .newAccount()
+            .fullname("committer")
+            .preferredEmail("committeremail@example.com")
+            .create();
+
+    Change.Id changeId =
+        changeOperations.newChange().author(authorId).committer(committerId).create();
+    ChangeInfo changeInfo = gApi.changes().id(changeId.get()).get();
+    assertCommitter(changeInfo, "committeremail@example.com");
+
+    allowLabelPermission(
+        codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
+
+    // Vote from committer does not match
+    requestScopeOperations.setApiUser(committerId);
+    approve(changeId.toString());
+    assertNotMatching("label:Code-Review=+2,user=non_contributor", changeId);
+  }
+
+  @Test
+  public void nonContributorLabelVote_uploaderAndAuthorDifferent() throws Exception {
+    TestRepository<InMemoryRepository> clonedRepo = cloneProject(project, admin);
+    PushOneCommit.Result r1 =
+        pushFactory
+            .create(user.newIdent(), clonedRepo, "Subject", "file.txt", "text")
+            .to("refs/for/master");
+
+    requestScopeOperations.setApiUser(admin.id());
+    ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
+    assertUploader(changeInfo, admin.email());
+    assertAuthor(changeInfo, user.email());
+
+    allowLabelPermission(
+        codeReview().getName(), RefNames.REFS_HEADS + "*", REGISTERED_USERS, -2, +2);
+
+    // Vote from admin (a.k.a. uploader) does not match
+    requestScopeOperations.setApiUser(user.id());
+    approve(r1.getChangeId());
+    assertNotMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
+
+    // Vote from user (a.k.a. author) does not match
+    requestScopeOperations.setApiUser(admin.id());
+    approve(r1.getChangeId());
+    assertNotMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
+
+    // Vote from user2 (a.k.a. non-author and non-uploader) matches
+    TestAccount user2 = accountCreator.create();
+    requestScopeOperations.setApiUser(user2.id());
+    approve(r1.getChangeId());
+    assertMatching("label:Code-Review=+2,user=non_contributor", r1.getChange().getId());
+  }
+
+  private static void assertUploader(ChangeInfo changeInfo, String email) {
+    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).uploader.email)
+        .isEqualTo(email);
+  }
+
+  private static void assertCommitter(ChangeInfo changeInfo, String email) {
+    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.committer.email)
+        .isEqualTo(email);
+  }
+
+  private static void assertAuthor(ChangeInfo changeInfo, String email) {
+    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.author.email)
+        .isEqualTo(email);
+  }
+
+  private void allowLabelPermission(
+      String labelName, String refPattern, AccountGroup.UUID group, int minVote, int maxVote) {
+    projectOperations
+        .project(project)
+        .forUpdate()
+        .add(allowLabel(labelName).ref(refPattern).group(group).range(minVote, maxVote))
+        .update();
+  }
+
   private PushOneCommit.Result createGitSubmoduleCommit(String ref) throws Exception {
     return pushFactory
         .create(admin.newIdent(), testRepo, "subject", ImmutableMap.of())
@@ -302,6 +468,13 @@
         .to(ref);
   }
 
+  private PushOneCommit.Result createNormalCommit(
+      PersonIdent personIdent, String ref, String fileName) throws Exception {
+    return pushFactory
+        .create(personIdent, testRepo, "subject", ImmutableMap.of(fileName, fileName))
+        .to(ref);
+  }
+
   private PushOneCommit.Result createNormalCommit(String ref, String fileName) throws Exception {
     return pushFactory
         .create(admin.newIdent(), testRepo, "subject", ImmutableMap.of(fileName, fileName))
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
index e6c3919..1607f09 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsConsistencyIT.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
@@ -90,7 +91,7 @@
     try (Repository repo = repoManager.openRepository(allUsers)) {
       RefUpdate ru = repo.updateRef(RefNames.REFS_GROUPNAMES);
       ru.setForceUpdate(true);
-      RefUpdate.Result result = ru.delete();
+      RefUpdate.Result result = testRefAction(() -> ru.delete());
       assertThat(result).isEqualTo(Result.FORCED);
     }
 
@@ -103,7 +104,7 @@
     try (Repository repo = repoManager.openRepository(allUsers)) {
       RefUpdate ru = repo.updateRef(RefNames.refsGroups(AccountGroup.uuid(g1.id)));
       ru.setForceUpdate(true);
-      RefUpdate.Result result = ru.delete();
+      RefUpdate.Result result = testRefAction(() -> ru.delete());
       assertThat(result).isEqualTo(Result.FORCED);
     }
 
@@ -117,7 +118,7 @@
       RefRename ru =
           repo.renameRef(
               RefNames.refsGroups(AccountGroup.uuid(g1.id)), RefNames.REFS_GROUPS + BOGUS_UUID);
-      RefUpdate.Result result = ru.rename();
+      RefUpdate.Result result = testRefAction(() -> ru.rename());
       assertThat(result).isEqualTo(Result.RENAMED);
     }
 
@@ -132,7 +133,7 @@
           repo.renameRef(
               RefNames.refsGroups(AccountGroup.uuid(g1.id)),
               RefNames.refsGroups(AccountGroup.uuid(BOGUS_UUID)));
-      RefUpdate.Result result = ru.rename();
+      RefUpdate.Result result = testRefAction(() -> ru.rename());
       assertThat(result).isEqualTo(Result.RENAMED);
     }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 04bdf15..142a45c 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -28,6 +28,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.gerrit.truth.MapSubject.assertThatMap;
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -1147,7 +1148,7 @@
       RefUpdate ru = repo.updateRef(RefNames.refsGroups(uuid));
       ru.setForceUpdate(true);
       ru.setNewObjectId(ObjectId.zeroId());
-      assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+      testRefAction(() -> assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED));
     }
 
     // Reindex the group.
@@ -1349,7 +1350,7 @@
         updateRef.setExpectedOldObjectId(commit.toObjectId());
         updateRef.setNewObjectId(ObjectId.zeroId());
         updateRef.setForceUpdate(true);
-        assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
+        testRefAction(() -> assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED));
       }
 
       // refs/meta/group-names is only visible with ACCESS_DATABASE
@@ -1449,7 +1450,7 @@
       RefUpdate updateRef = repo.updateRef(groupRef);
       updateRef.setExpectedOldObjectId(commit.toObjectId());
       updateRef.setNewObjectId(emptyCommit);
-      assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
+      testRefAction(() -> assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED));
     }
     assertStaleGroupAndReindex(groupUuid);
 
@@ -1461,7 +1462,7 @@
       updateRef.setExpectedOldObjectId(commit.toObjectId());
       updateRef.setNewObjectId(ObjectId.zeroId());
       updateRef.setForceUpdate(true);
-      assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
+      testRefAction(() -> assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED));
     }
     assertStaleGroupAndReindex(groupUuid);
   }
@@ -1506,13 +1507,15 @@
       // then run the reindexer -> only the new group is reindexed.
       String groupName = "foo";
       AccountGroup.UUID groupUuid = AccountGroup.uuid(groupName + "-UUID");
-      groupsUpdate.createGroupInNoteDb(
-          InternalGroupCreation.builder()
-              .setGroupUUID(groupUuid)
-              .setNameKey(AccountGroup.nameKey(groupName))
-              .setId(AccountGroup.id(seq.nextGroupId()))
-              .build(),
-          GroupDelta.builder().build());
+      testRefAction(
+          () ->
+              groupsUpdate.createGroupInNoteDb(
+                  InternalGroupCreation.builder()
+                      .setGroupUUID(groupUuid)
+                      .setNameKey(AccountGroup.nameKey(groupName))
+                      .setId(AccountGroup.id(seq.nextGroupId()))
+                      .build(),
+                  GroupDelta.builder().build()));
       slaveGroupIndexer.run();
       groupIndexedCounter.assertReindexOf(groupUuid);
 
@@ -1528,7 +1531,7 @@
       try (Repository repo = repoManager.openRepository(allUsers)) {
         RefUpdate u = repo.updateRef(RefNames.refsGroups(groupUuid));
         u.setForceUpdate(true);
-        assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
+        testRefAction(() -> assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED));
       }
       slaveGroupIndexer.run();
       groupIndexedCounter.assertReindexOf(groupUuid);
@@ -1613,7 +1616,7 @@
       RefUpdate updateRef = r.updateRef(ref);
       updateRef.setExpectedOldObjectId(ObjectId.zeroId());
       updateRef.setNewObjectId(emptyCommit);
-      assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.NEW);
+      testRefAction(() -> assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.NEW));
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
index 6bd2b68..a2f1f46 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/AccessIT.java
@@ -22,6 +22,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.schema.AclUtil.grant;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.gerrit.truth.ConfigSubject.assertThat;
 import static com.google.gerrit.truth.MapSubject.assertThatMap;
 import static java.util.Arrays.asList;
@@ -240,7 +241,7 @@
         Registration registration = newFileHistoryWebLink()) {
       RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG);
       u.setForceUpdate(true);
-      assertThat(u.delete()).isEqualTo(Result.FORCED);
+      testRefAction(() -> assertThat(u.delete()).isEqualTo(Result.FORCED));
 
       // This should not crash.
       pApi().access();
@@ -442,7 +443,7 @@
     try (Repository repo = repoManager.openRepository(newProjectName)) {
       RefUpdate ru = repo.updateRef(RefNames.REFS_CONFIG);
       ru.setForceUpdate(true);
-      assertThat(ru.delete()).isEqualTo(Result.FORCED);
+      testRefAction(() -> assertThat(ru.delete()).isEqualTo(Result.FORCED));
 
       ProjectAccessInput accessInput = newProjectAccessInput();
       AccessSectionInfo accessSection = newAccessSectionInfo();
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index b0de1c1..5c46fec 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -388,7 +389,7 @@
     try (Repository repo = repoManager.openRepository(normalProject)) {
       RefUpdate u = repo.updateRef(RefNames.REFS_HEADS + "master");
       u.setForceUpdate(true);
-      assertThat(u.delete()).isEqualTo(Result.FORCED);
+      testRefAction(() -> assertThat(u.delete()).isEqualTo(Result.FORCED));
     }
     AccessCheckInput input = new AccessCheckInput();
     input.account = privilegedUser.email();
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index b738324..e120f97 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -41,6 +41,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.server.project.testing.TestLabels.label;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.util.Comparator.comparing;
 import static java.util.stream.Collectors.joining;
 import static java.util.stream.Collectors.toList;
@@ -335,7 +336,7 @@
       RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG);
       u.setForceUpdate(true);
       u.setExpectedOldObjectId(repo.resolve(RefNames.REFS_CONFIG));
-      assertThat(u.delete(rw)).isEqualTo(Result.FORCED);
+      testRefAction(() -> assertThat(u.delete(rw)).isEqualTo(Result.FORCED));
     }
 
     RevCommit c =
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index c3bcbd3..206a9d5 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
@@ -511,7 +512,7 @@
       RefUpdate ru = serverRepo.updateRef(refName);
       ru.setExpectedOldObjectId(oldCommitId);
       ru.setNewObjectId(newCommitId);
-      assertThat(ru.update()).isEqualTo(RefUpdate.Result.FAST_FORWARD);
+      testRefAction(() -> assertThat(ru.update()).isEqualTo(RefUpdate.Result.FAST_FORWARD));
     }
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java b/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java
index b16394d..3b158a9 100644
--- a/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/AutoMergeIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 
 import com.google.common.collect.ImmutableList;
@@ -201,7 +202,7 @@
     try (Repository repo = repoManager.openRepository(project)) {
       RefUpdate ru = repo.updateRef(RefNames.refsCacheAutomerge(mergeCommit.name()));
       ru.setForceUpdate(true);
-      assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+      testRefAction(() -> assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED));
     }
     assertNoAutoMergeCreated(mergeCommit);
   }
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index f58f81c..9e85d8c 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -24,6 +24,7 @@
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
@@ -217,7 +218,7 @@
       RefUpdate mtu = repo.updateRef("refs/tags/master-tag");
       mtu.setExpectedOldObjectId(ObjectId.zeroId());
       mtu.setNewObjectId(repo.exactRef("refs/heads/master").getObjectId());
-      assertThat(mtu.update()).isEqualTo(RefUpdate.Result.NEW);
+      testRefAction(() -> assertThat(mtu.update()).isEqualTo(RefUpdate.Result.NEW));
 
       //   rcMaster (c1 master master-tag) <-- rcBranch (c2 branch branch-tag)
       //       \                                  \
@@ -225,14 +226,14 @@
       RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
       btu.setExpectedOldObjectId(ObjectId.zeroId());
       btu.setNewObjectId(repo.exactRef("refs/heads/branch").getObjectId());
-      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+      testRefAction(() -> assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW));
 
       // Create a tag for the tree of the commit on 'master'
       // tree-tag -> master.tree
       RefUpdate ttu = repo.updateRef("refs/tags/tree-tag");
       ttu.setExpectedOldObjectId(ObjectId.zeroId());
       ttu.setNewObjectId(rcMaster.getTree().toObjectId());
-      assertThat(ttu.update()).isEqualTo(RefUpdate.Result.NEW);
+      testRefAction(() -> assertThat(ttu.update()).isEqualTo(RefUpdate.Result.NEW));
     }
   }
 
@@ -588,14 +589,17 @@
         .forUpdate()
         .add(allow(Permission.READ).ref("refs/heads/branch").group(REGISTERED_USERS))
         .update();
-    // Create a tag for the pending change on 'branch' so that the tag is orphaned
-    try (Repository repo = repoManager.openRepository(project)) {
-      // change4-tag -> psRef4
-      RefUpdate ctu = repo.updateRef("refs/tags/change4-tag");
-      ctu.setExpectedOldObjectId(ObjectId.zeroId());
-      ctu.setNewObjectId(repo.exactRef(psRef4).getObjectId());
-      assertThat(ctu.update()).isEqualTo(RefUpdate.Result.NEW);
-    }
+    testRefAction(
+        () -> {
+          // Create a tag for the pending change on 'branch' so that the tag is orphaned
+          try (Repository repo = repoManager.openRepository(project)) {
+            // change4-tag -> psRef4
+            RefUpdate ctu = repo.updateRef("refs/tags/change4-tag");
+            ctu.setExpectedOldObjectId(ObjectId.zeroId());
+            ctu.setNewObjectId(repo.exactRef(psRef4).getObjectId());
+            assertThat(ctu.update()).isEqualTo(RefUpdate.Result.NEW);
+          }
+        });
 
     requestScopeOperations.setApiUser(user.id());
     assertUploadPackRefs(
@@ -641,7 +645,7 @@
       RefUpdate btu = repo.updateRef("refs/tags/master-newtag");
       btu.setExpectedOldObjectId(ObjectId.zeroId());
       btu.setNewObjectId(r.getCommit());
-      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+      testRefAction(() -> assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW));
     }
 
     assertUploadPackRefs(
@@ -695,7 +699,7 @@
       RefUpdate btu = repo.updateRef("refs/tags/branch-newtag");
       btu.setExpectedOldObjectId(ObjectId.zeroId());
       btu.setNewObjectId(tagRc);
-      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+      testRefAction(() -> assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW));
     }
 
     assertUploadPackRefs(
@@ -751,7 +755,7 @@
       RefUpdate btu = repo.updateRef("refs/tags/branch-newtag");
       btu.setExpectedOldObjectId(ObjectId.zeroId());
       btu.setNewObjectId(tagRc);
-      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+      testRefAction(() -> assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW));
     }
 
     assertUploadPackRefs(
@@ -794,10 +798,11 @@
       RevCommit bRc = r.getCommit();
 
       // rcBranch (c2) <- newcommit1 (branch-oldtag) <- newcommit2 (branch)
-      RefUpdate btu = repo.updateRef("refs/tags/branch-oldtag");
-      btu.setExpectedOldObjectId(ObjectId.zeroId());
-      btu.setNewObjectId(tagRc);
-      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+      RefUpdate btu1 = repo.updateRef("refs/tags/branch-oldtag");
+
+      btu1.setExpectedOldObjectId(ObjectId.zeroId());
+      btu1.setNewObjectId(tagRc);
+      testRefAction(() -> assertThat(btu1.update()).isEqualTo(RefUpdate.Result.NEW));
 
       assertUploadPackRefs(
           psRef2,
@@ -811,11 +816,11 @@
           "refs/tags/master-tag");
 
       // rcBranch (c2 branch) <- newcommit1 (branch-oldtag) <- newcommit2
-      btu = repo.updateRef("refs/heads/branch");
-      btu.setExpectedOldObjectId(bRc);
-      btu.setNewObjectId(rcBranch);
-      btu.setForceUpdate(true);
-      assertThat(btu.update()).isEqualTo(RefUpdate.Result.FORCED);
+      RefUpdate btu2 = repo.updateRef("refs/heads/branch");
+      btu2.setExpectedOldObjectId(bRc);
+      btu2.setNewObjectId(rcBranch);
+      btu2.setForceUpdate(true);
+      testRefAction(() -> assertThat(btu2.update()).isEqualTo(RefUpdate.Result.FORCED));
     }
 
     assertUploadPackRefs(
@@ -907,7 +912,7 @@
       btu.setExpectedOldObjectId(tagRc);
       btu.setNewObjectId(rcBranch);
       btu.setForceUpdate(true);
-      assertThat(btu.update()).isEqualTo(RefUpdate.Result.FORCED);
+      testRefAction(() -> assertThat(btu.update()).isEqualTo(RefUpdate.Result.FORCED));
     }
 
     assertUploadPackRefs(
@@ -939,7 +944,7 @@
       RefUpdate btu = repo.updateRef("refs/tags/updated-tag");
       btu.setExpectedOldObjectId(ObjectId.zeroId());
       btu.setNewObjectId(rcBranch);
-      assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW);
+      testRefAction(() -> assertThat(btu.update()).isEqualTo(RefUpdate.Result.NEW));
 
       assertUploadPackRefs(
           psRef2,
@@ -995,13 +1000,16 @@
         "refs/tags/master-tag");
 
     // rcBranch (c2 branch)
-    try (Repository repo = repoManager.openRepository(project)) {
-      RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
-      btu.setExpectedOldObjectId(rcBranch);
-      btu.setNewObjectId(ObjectId.zeroId());
-      btu.setForceUpdate(true);
-      assertThat(btu.delete()).isEqualTo(RefUpdate.Result.FORCED);
-    }
+    testRefAction(
+        () -> {
+          try (Repository repo = repoManager.openRepository(project)) {
+            RefUpdate btu = repo.updateRef("refs/tags/branch-tag");
+            btu.setExpectedOldObjectId(rcBranch);
+            btu.setNewObjectId(ObjectId.zeroId());
+            btu.setForceUpdate(true);
+            assertThat(btu.delete()).isEqualTo(RefUpdate.Result.FORCED);
+          }
+        });
 
     assertUploadPackRefs(
         psRef2, metaRef2, psRef4, metaRef4, "refs/heads/branch", "refs/tags/master-tag");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 0e4f212..f9fb92c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -95,6 +95,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.context.RefUpdateContext;
+import com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testing.ConfigSuite;
@@ -1125,20 +1127,22 @@
   }
 
   private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Throwable {
-    for (PushOneCommit.Result change : changes) {
-      try (BatchUpdate bu =
-          batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.now())) {
-        bu.addOp(
-            change.getChange().getId(),
-            new BatchUpdateOp() {
-              @Override
-              public boolean updateChange(ChangeContext ctx) {
-                ctx.getChange().setStatus(Change.Status.NEW);
-                ctx.getUpdate(ctx.getChange().currentPatchSetId()).setStatus(Change.Status.NEW);
-                return true;
-              }
-            });
-        bu.execute();
+    try (RefUpdateContext ctx = RefUpdateContext.open(RefUpdateType.CHANGE_MODIFICATION)) {
+      for (PushOneCommit.Result change : changes) {
+        try (BatchUpdate bu =
+            batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.now())) {
+          bu.addOp(
+              change.getChange().getId(),
+              new BatchUpdateOp() {
+                @Override
+                public boolean updateChange(ChangeContext ctx) {
+                  ctx.getChange().setStatus(Change.Status.NEW);
+                  ctx.getUpdate(ctx.getChange().currentPatchSetId()).setStatus(Change.Status.NEW);
+                  return true;
+                }
+              });
+          bu.execute();
+        }
       }
     }
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 079f84e..d08a7db 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -27,6 +27,7 @@
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
@@ -116,16 +117,19 @@
 
   @Before
   public void addNonCommitHead() throws Exception {
-    try (Repository repo = repoManager.openRepository(project);
-        ObjectInserter ins = repo.newObjectInserter()) {
-      ObjectId answer = ins.insert(Constants.OBJ_BLOB, new byte[] {42});
-      ins.flush();
-      ins.close();
+    testRefAction(
+        () -> {
+          try (Repository repo = repoManager.openRepository(project);
+              ObjectInserter ins = repo.newObjectInserter()) {
+            ObjectId answer = ins.insert(Constants.OBJ_BLOB, new byte[] {42});
+            ins.flush();
+            ins.close();
 
-      RefUpdate update = repo.getRefDatabase().newUpdate("refs/heads/answer", false);
-      update.setNewObjectId(answer);
-      assertThat(update.forceUpdate()).isEqualTo(RefUpdate.Result.NEW);
-    }
+            RefUpdate update = repo.getRefDatabase().newUpdate("refs/heads/answer", false);
+            update.setNewObjectId(answer);
+            assertThat(update.forceUpdate()).isEqualTo(RefUpdate.Result.NEW);
+          }
+        });
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/FileBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
index 8dce9c3..8c8f267 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/FileBranchIT.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -58,7 +59,7 @@
   @Test
   public void getFileFromSymbolicRefPointingToAnUnbornBranch() throws Exception {
     try (Repository repo = repoManager.openRepository(project)) {
-      repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing");
+      testRefAction(() -> repo.updateRef(Constants.HEAD, true).link("refs/heads/non-existing"));
     }
     RestResponse response =
         adminRestSession.get(String.format("/projects/%s/branches/HEAD/files/path", project.get()));
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index bcde618..55f102f 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -17,6 +17,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIXED;
 import static com.google.gerrit.extensions.common.ProblemInfo.Status.FIX_FAILED;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.gerrit.testing.TestChanges.newPatchSet;
 import static java.util.Objects.requireNonNull;
 
@@ -47,6 +49,7 @@
 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.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestChanges;
 import com.google.inject.Inject;
@@ -297,7 +300,7 @@
     serverSideTestRepo.reset(serverSideTestRepo.getRepository().exactRef(ref).getObjectId());
     RefUpdate ru = serverSideTestRepo.getRepository().updateRef(ref);
     ru.setForceUpdate(true);
-    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+    testRefAction(() -> assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED));
 
     assertProblems(notes, null, problem("Destination ref not found (may be new branch): " + ref));
   }
@@ -305,20 +308,21 @@
   @Test
   public void mergedChangeIsNotMerged() throws Exception {
     ChangeNotes notes = insertChange();
-
-    try (BatchUpdate bu = newUpdate(adminId)) {
-      bu.addOp(
-          notes.getChangeId(),
-          new BatchUpdateOp() {
-            @Override
-            public boolean updateChange(ChangeContext ctx) {
-              ctx.getChange().setStatus(Change.Status.MERGED);
-              ctx.getUpdate(ctx.getChange().currentPatchSetId())
-                  .fixStatusToMerged(new SubmissionId(ctx.getChange()));
-              return true;
-            }
-          });
-      bu.execute();
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (BatchUpdate bu = newUpdate(adminId)) {
+        bu.addOp(
+            notes.getChangeId(),
+            new BatchUpdateOp() {
+              @Override
+              public boolean updateChange(ChangeContext ctx) {
+                ctx.getChange().setStatus(Change.Status.MERGED);
+                ctx.getUpdate(ctx.getChange().currentPatchSetId())
+                    .fixStatusToMerged(new SubmissionId(ctx.getChange()));
+                return true;
+              }
+            });
+        bu.execute();
+      }
     }
     notes = reload(notes);
 
@@ -745,19 +749,22 @@
 
   private ChangeNotes insertChange(TestAccount owner, String dest) throws Exception {
     Change.Id id = Change.id(sequences.nextChangeId());
-    ChangeInserter ins;
-    try (BatchUpdate bu = newUpdate(owner.id())) {
-      RevCommit commit = patchSetCommit(PatchSet.id(id, 1));
-      bu.setNotify(NotifyResolver.Result.none());
-      ins =
-          changeInserterFactory
-              .create(id, commit, dest)
-              .setValidate(false)
-              .setFireRevisionCreated(false)
-              .setSendMail(false);
-      bu.insertChange(ins).execute();
-    }
-    return changeNotesFactory.create(project, ins.getChange().getId());
+    return testRefAction(
+        () -> {
+          ChangeInserter ins;
+          try (BatchUpdate bu = newUpdate(owner.id())) {
+            RevCommit commit = patchSetCommit(PatchSet.id(id, 1));
+            bu.setNotify(NotifyResolver.Result.none());
+            ins =
+                changeInserterFactory
+                    .create(id, commit, dest)
+                    .setValidate(false)
+                    .setFireRevisionCreated(false)
+                    .setSendMail(false);
+            bu.insertChange(ins).execute();
+          }
+          return changeNotesFactory.create(project, ins.getChange().getId());
+        });
   }
 
   private PatchSet.Id nextPatchSetId(ChangeNotes notes) throws Exception {
@@ -770,17 +777,20 @@
   }
 
   private ChangeNotes incrementPatchSet(ChangeNotes notes, RevCommit commit) throws Exception {
-    PatchSetInserter ins;
-    try (BatchUpdate bu = newUpdate(notes.getChange().getOwner())) {
-      bu.setNotify(NotifyResolver.Result.none());
-      ins =
-          patchSetInserterFactory
-              .create(notes, nextPatchSetId(notes), commit)
-              .setValidate(false)
-              .setFireRevisionCreated(false);
-      bu.addOp(notes.getChangeId(), ins).execute();
-    }
-    return reload(notes);
+    return testRefAction(
+        () -> {
+          PatchSetInserter ins;
+          try (BatchUpdate bu = newUpdate(notes.getChange().getOwner())) {
+            bu.setNotify(NotifyResolver.Result.none());
+            ins =
+                patchSetInserterFactory
+                    .create(notes, nextPatchSetId(notes), commit)
+                    .setValidate(false)
+                    .setFireRevisionCreated(false);
+            bu.addOp(notes.getChangeId(), ins).execute();
+          }
+          return reload(notes);
+        });
   }
 
   private ChangeNotes reload(ChangeNotes notes) throws Exception {
@@ -822,7 +832,7 @@
   private void deleteRef(String refName) throws Exception {
     RefUpdate ru = serverSideTestRepo.getRepository().updateRef(refName, true);
     ru.setForceUpdate(true);
-    assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
+    testRefAction(() -> assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED));
   }
 
   private void addNoteDbCommit(Change.Id id, String commitMessage) throws Exception {
@@ -847,30 +857,33 @@
   }
 
   private ChangeNotes mergeChange(ChangeNotes notes) throws Exception {
-    ObjectId oldId = getDestRef(notes);
-    ObjectId newId = psUtil.current(notes).commitId();
-    String dest = notes.getChange().getDest().branch();
+    return testRefAction(
+        () -> {
+          ObjectId oldId = getDestRef(notes);
+          ObjectId newId = psUtil.current(notes).commitId();
+          String dest = notes.getChange().getDest().branch();
 
-    try (BatchUpdate bu = newUpdate(adminId)) {
-      bu.addOp(
-          notes.getChangeId(),
-          new BatchUpdateOp() {
-            @Override
-            public void updateRepo(RepoContext ctx) throws IOException {
-              ctx.addRefUpdate(oldId, newId, dest);
-            }
+          try (BatchUpdate bu = newUpdate(adminId)) {
+            bu.addOp(
+                notes.getChangeId(),
+                new BatchUpdateOp() {
+                  @Override
+                  public void updateRepo(RepoContext ctx) throws IOException {
+                    ctx.addRefUpdate(oldId, newId, dest);
+                  }
 
-            @Override
-            public boolean updateChange(ChangeContext ctx) {
-              ctx.getChange().setStatus(Change.Status.MERGED);
-              ctx.getUpdate(ctx.getChange().currentPatchSetId())
-                  .fixStatusToMerged(new SubmissionId(ctx.getChange()));
-              return true;
-            }
-          });
-      bu.execute();
-    }
-    return reload(notes);
+                  @Override
+                  public boolean updateChange(ChangeContext ctx) {
+                    ctx.getChange().setStatus(Change.Status.MERGED);
+                    ctx.getUpdate(ctx.getChange().currentPatchSetId())
+                        .fixStatusToMerged(new SubmissionId(ctx.getChange()));
+                    return true;
+                  }
+                });
+            bu.execute();
+          }
+          return reload(notes);
+        });
   }
 
   private static ProblemInfo problem(String message) {
@@ -911,7 +924,7 @@
       ru.setExpectedOldObjectId(ref.getObjectId());
       ru.setNewObjectId(ObjectId.zeroId());
       ru.setForceUpdate(true);
-      Result result = ru.delete();
+      Result result = testRefAction(() -> ru.delete());
       if (result != Result.FORCED) {
         throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name()));
       }
diff --git a/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java b/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
index 1eef944..107b777 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/DeleteZombieDraftIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableList;
@@ -30,6 +31,7 @@
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.server.notedb.ChangeNoteJson;
 import com.google.gerrit.server.notedb.DeleteZombieCommentsRefs;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.gson.JsonParser;
 import com.google.inject.Inject;
@@ -191,10 +193,12 @@
   }
 
   private void restoreRef(String refName, ObjectId id) throws Exception {
-    try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
-      RefUpdate u = allUsersRepo.updateRef(refName);
-      u.setNewObjectId(id);
-      u.forceUpdate();
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (Repository allUsersRepo = repoManager.openRepository(allUsers)) {
+        RefUpdate u = allUsersRepo.updateRef(refName);
+        u.setNewObjectId(id);
+        u.forceUpdate();
+      }
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 21db45c..3e03b2a 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
@@ -55,6 +56,7 @@
 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.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.ConfigSuite;
 import com.google.inject.Inject;
@@ -772,17 +774,19 @@
   }
 
   private void clearGroups(PatchSet.Id psId) throws Exception {
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user(user), TimeUtil.now())) {
-      bu.addOp(
-          psId.changeId(),
-          new BatchUpdateOp() {
-            @Override
-            public boolean updateChange(ChangeContext ctx) {
-              ctx.getUpdate(psId).setGroups(ImmutableList.of());
-              return true;
-            }
-          });
-      bu.execute();
+    try (RefUpdateContext ctx = openTestRefUpdateContext()) {
+      try (BatchUpdate bu = batchUpdateFactory.create(project, user(user), TimeUtil.now())) {
+        bu.addOp(
+            psId.changeId(),
+            new BatchUpdateOp() {
+              @Override
+              public boolean updateChange(ChangeContext ctx) {
+                ctx.getUpdate(psId).setGroups(ImmutableList.of());
+                return true;
+              }
+            });
+        bu.execute();
+      }
     }
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
index 45a471b..f728995 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailSenderIT.java
@@ -15,16 +15,25 @@
 package com.google.gerrit.acceptance.server.mail;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.entities.EmailHeader;
 import com.google.gerrit.entities.EmailHeader.StringEmailHeader;
+import com.google.gerrit.server.config.SitePaths;
 import java.net.URI;
+import java.nio.file.Files;
 import java.util.Map;
+import javax.inject.Inject;
 import org.junit.Test;
 
+@UseLocalDisk
 public class MailSenderIT extends AbstractMailIT {
 
+  @Inject private SitePaths sitePaths;
+
   @Test
   @GerritConfig(name = "sendemail.replyToAddress", value = "custom@gerritcodereview.com")
   @GerritConfig(name = "receiveemail.protocol", value = "POP3")
@@ -63,6 +72,20 @@
     assertThat(headerString(headers, "In-Reply-To")).isEqualTo(threadId);
   }
 
+  @Test
+  @Sandboxed
+  public void useCustomTemplates() throws Exception {
+    String customTemplate =
+        "{namespace com.google.gerrit.server.mail.template.ChangeSubject}\n"
+            + "\n"
+            + "{template ChangeSubject kind=\"text\"}CUSTOM-TEMPLATE{/template}\n";
+    Files.write(sitePaths.mail_dir.resolve("ChangeSubject.soy"), customTemplate.getBytes(UTF_8));
+
+    createChangeWithReview(user);
+    String subject = headerString(sender.getMessages().iterator().next().headers(), "Subject");
+    assertThat(subject).isEqualTo("CUSTOM-TEMPLATE");
+  }
+
   private String headerString(Map<String, EmailHeader> headers, String name) {
     EmailHeader header = headers.get(name);
     assertThat(header).isInstanceOf(StringEmailHeader.class);
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index ab5e1d8..fc746ad 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.Iterables;
@@ -100,10 +101,13 @@
           }
         };
 
-    try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
-      bu.addOp(id, backupMasterOp);
-      bu.execute();
-    }
+    testRefAction(
+        () -> {
+          try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
+            bu.addOp(id, backupMasterOp);
+            bu.execute();
+          }
+        });
 
     // Ensure backupMasterOp worked.
     assertThat(getRef(backup)).hasValue(master1);
@@ -158,13 +162,16 @@
             .changeUpdate(
                 "testUpdateRefAndAddMessageOp",
                 batchUpdateFactory -> {
-                  try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
-                    bu.addOp(
-                        id,
-                        new UpdateRefAndAddMessageOp(
-                            updateRepoCalledCount, updateChangeCalledCount));
-                    bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount));
-                  }
+                  testRefAction(
+                      () -> {
+                        try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
+                          bu.addOp(
+                              id,
+                              new UpdateRefAndAddMessageOp(
+                                  updateRepoCalledCount, updateChangeCalledCount));
+                          bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount));
+                        }
+                      });
                   return "Done";
                 })
             .call();
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index c694a87..b5c6149 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -71,6 +71,7 @@
         "//java/com/google/gerrit/sshd",
         "//java/com/google/gerrit/testing:assertable-executor",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//java/com/google/gerrit/truth",
         "//lib:gson",
         "//lib:guava",
diff --git a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
index 6bdf80f..0112f88 100644
--- a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
+++ b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.entities.Account;
@@ -166,7 +167,7 @@
           new ReceiveCommand(ObjectId.zeroId(), commitId, refName, ReceiveCommand.Type.CREATE));
       refNames.add(refName);
     }
-    RefUpdateUtil.executeChecked(bru, usersRepo);
+    testRefAction(() -> RefUpdateUtil.executeChecked(bru, usersRepo));
     return refNames;
   }
 
@@ -201,7 +202,7 @@
     RefUpdate update = repo.updateRef(refName);
     update.setNewObjectId(commitId);
     update.setForceUpdate(true);
-    update.update();
+    testRefAction(() -> update.update());
     return repo.exactRef(refName);
   }
 
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
index a764654..b2a6790 100644
--- a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.group.db;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -236,34 +237,40 @@
 
   private InternalGroup createGroup(
       int next, String groupName, PersonIdent authorIdent, Account.Id authorId) throws Exception {
-    InternalGroupCreation groupCreation =
-        InternalGroupCreation.builder()
-            .setGroupUUID(GroupUuid.make(groupName, serverIdent))
-            .setNameKey(AccountGroup.nameKey(groupName))
-            .setId(AccountGroup.id(next))
-            .build();
-    GroupDelta groupDelta =
-        authorIdent.equals(serverIdent)
-            ? GroupDelta.builder().setDescription("Groups").build()
-            : GroupDelta.builder()
-                .setDescription("Groups")
-                .setMemberModification(members -> ImmutableSet.of(authorId))
-                .build();
+    return testRefAction(
+        () -> {
+          InternalGroupCreation groupCreation =
+              InternalGroupCreation.builder()
+                  .setGroupUUID(GroupUuid.make(groupName, serverIdent))
+                  .setNameKey(AccountGroup.nameKey(groupName))
+                  .setId(AccountGroup.id(next))
+                  .build();
+          GroupDelta groupDelta =
+              authorIdent.equals(serverIdent)
+                  ? GroupDelta.builder().setDescription("Groups").build()
+                  : GroupDelta.builder()
+                      .setDescription("Groups")
+                      .setMemberModification(members -> ImmutableSet.of(authorId))
+                      .build();
 
-    GroupConfig groupConfig =
-        GroupConfig.createForNewGroup(allUsersName, allUsersRepo, groupCreation);
-    groupConfig.setGroupDelta(groupDelta, getAuditLogFormatter());
+          GroupConfig groupConfig =
+              GroupConfig.createForNewGroup(allUsersName, allUsersRepo, groupCreation);
+          groupConfig.setGroupDelta(groupDelta, getAuditLogFormatter());
 
-    groupConfig.commit(createMetaDataUpdate(authorIdent));
-    return groupConfig
-        .getLoadedGroup()
-        .orElseThrow(() -> new IllegalStateException("create group failed"));
+          groupConfig.commit(createMetaDataUpdate(authorIdent));
+          return groupConfig
+              .getLoadedGroup()
+              .orElseThrow(() -> new IllegalStateException("create group failed"));
+        });
   }
 
   private void updateGroup(AccountGroup.UUID uuid, GroupDelta groupDelta) throws Exception {
-    GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, uuid);
-    groupConfig.setGroupDelta(groupDelta, getAuditLogFormatter());
-    groupConfig.commit(createMetaDataUpdate(userIdent));
+    testRefAction(
+        () -> {
+          GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, uuid);
+          groupConfig.setGroupDelta(groupDelta, getAuditLogFormatter());
+          groupConfig.commit(createMetaDataUpdate(userIdent));
+        });
   }
 
   private void addMembers(AccountGroup.UUID groupUuid, Set<Account.Id> ids) throws Exception {
diff --git a/javatests/com/google/gerrit/server/group/db/BUILD b/javatests/com/google/gerrit/server/group/db/BUILD
index 9f9f459..47550bb 100644
--- a/javatests/com/google/gerrit/server/group/db/BUILD
+++ b/javatests/com/google/gerrit/server/group/db/BUILD
@@ -17,6 +17,7 @@
         "//java/com/google/gerrit/server/group/testing",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//java/com/google/gerrit/truth",
         "//lib:guava",
         "//lib:jgit",
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 1e6ba3a..bf6839d 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static com.google.inject.Scopes.SINGLETON;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -247,13 +248,16 @@
   }
 
   protected Change newChange(Injector injector, boolean workInProgress) throws Exception {
-    Change c = TestChanges.newChange(project, changeOwner.getAccountId());
-    ChangeUpdate u = newUpdate(injector, c, changeOwner, false);
-    u.setChangeId(c.getKey().get());
-    u.setBranch(c.getDest().branch());
-    u.setWorkInProgress(workInProgress);
-    u.commit();
-    return c;
+    return testRefAction(
+        () -> {
+          Change c = TestChanges.newChange(project, changeOwner.getAccountId());
+          ChangeUpdate u = newUpdate(injector, c, changeOwner, false);
+          u.setChangeId(c.getKey().get());
+          u.setBranch(c.getDest().branch());
+          u.setWorkInProgress(workInProgress);
+          u.commit();
+          return c;
+        });
   }
 
   protected Change newWorkInProgressChange() throws Exception {
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index cf739f6..15eefcd 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -25,6 +25,7 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REMOVED;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Comparator.comparing;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -1113,7 +1114,7 @@
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.putReviewer(changeOwner.getAccount().id(), REVIEWER);
     update.putReviewer(otherUser.getAccount().id(), CC);
-    update.commit();
+    testRefAction(() -> update.commit());
 
     ChangeNotes notes = newNotes(c);
     Instant ts = update.getWhen();
@@ -1936,7 +1937,7 @@
     try (NoteDbUpdateManager updateManager = updateManagerFactory.create(project)) {
       updateManager.add(update1);
       updateManager.add(update2);
-      updateManager.execute();
+      testRefAction(() -> updateManager.execute());
     }
 
     ChangeNotes notes = newNotes(c);
@@ -1985,7 +1986,7 @@
       update2.putApproval(LabelId.CODE_REVIEW, (short) 2);
       updateManager.add(update2);
 
-      updateManager.execute();
+      testRefAction(() -> updateManager.execute());
     }
 
     ChangeNotes notes = newNotes(c);
@@ -2047,7 +2048,7 @@
     try (NoteDbUpdateManager updateManager = updateManagerFactory.create(project)) {
       updateManager.add(update1);
       updateManager.add(update2);
-      updateManager.execute();
+      testRefAction(() -> updateManager.execute());
     }
 
     Ref ref1 = repo.exactRef(update1.getRefName());
@@ -3362,7 +3363,7 @@
     draftUpdate.putComment(comment2);
     try (NoteDbUpdateManager manager = updateManagerFactory.create(c.getProject())) {
       manager.add(draftUpdate);
-      manager.execute();
+      testRefAction(() -> manager.execute());
     }
 
     // Looking at drafts directly shows the zombie comment.
@@ -3426,7 +3427,7 @@
     try (NoteDbUpdateManager manager = updateManagerFactory.create(project)) {
       manager.add(update1);
       manager.add(update2);
-      manager.execute();
+      testRefAction(() -> manager.execute());
     }
 
     ChangeNotes notes = newNotes(c);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 4f4911a..d13ccdd 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -21,6 +21,7 @@
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REMOVED;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
@@ -90,7 +91,7 @@
       bru.addCommand(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
     }
 
-    RefUpdateUtil.executeChecked(bru, repo);
+    testRefAction(() -> RefUpdateUtil.executeChecked(bru, repo));
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
index 0c9f731..1b2d906 100644
--- a/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/javatests/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
@@ -385,7 +386,9 @@
       ins.flush();
       RefUpdate ru = repo.updateRef(refName);
       ru.setNewObjectId(newId);
-      assertThat(ru.forceUpdate()).isAnyOf(RefUpdate.Result.NEW, RefUpdate.Result.FORCED);
+      testRefAction(
+          () ->
+              assertThat(ru.forceUpdate()).isAnyOf(RefUpdate.Result.NEW, RefUpdate.Result.FORCED));
       return newId;
     } catch (IOException e) {
       throw new RuntimeException(e);
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 96a8dea..9070ffc 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -26,6 +26,7 @@
 import static com.google.gerrit.server.project.testing.TestLabels.label;
 import static com.google.gerrit.server.project.testing.TestLabels.value;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -1435,6 +1436,15 @@
     // "count" and "group" args cannot be used simultaneously.
     assertThrows(
         BadRequestException.class, () -> assertQuery("label:Code-Review=+1,group=gerrit,count=2"));
+
+    // "non_contributor arg for the label operator is not allowed in change queries
+    thrown =
+        assertThrows(
+            BadRequestException.class,
+            () -> assertQuery("label:Code-Review=+2,user=non_contributor"));
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo("non_contributor arg is not allowed in change queries");
   }
 
   @Test
@@ -4161,11 +4171,14 @@
     Project.NameKey project = Project.nameKey(repoName);
     Account.Id ownerId = owner != null ? owner : userId;
     IdentifiedUser user = userFactory.create(ownerId);
-    try (BatchUpdate bu = updateFactory.create(project, user, createdOn)) {
-      bu.insertChange(ins);
-      bu.execute();
-      return ins.getChange();
-    }
+    return testRefAction(
+        () -> {
+          try (BatchUpdate bu = updateFactory.create(project, user, createdOn)) {
+            bu.insertChange(ins);
+            bu.execute();
+            return ins.getChange();
+          }
+        });
   }
 
   protected Change newPatchSet(
@@ -4187,15 +4200,18 @@
               .create(changeNotesFactory.createChecked(c), PatchSet.id(c.getId(), n), commit)
               .setFireRevisionCreated(false)
               .setValidate(false);
-      try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.now());
-          ObjectInserter oi = repo.getRepository().newObjectInserter();
-          ObjectReader reader = oi.newReader();
-          RevWalk rw = new RevWalk(reader)) {
-        bu.setRepository(repo.getRepository(), rw, oi);
-        bu.setNotify(NotifyResolver.Result.none());
-        bu.addOp(c.getId(), inserter);
-        bu.execute();
-      }
+      testRefAction(
+          () -> {
+            try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.now());
+                ObjectInserter oi = repo.getRepository().newObjectInserter();
+                ObjectReader reader = oi.newReader();
+                RevWalk rw = new RevWalk(reader)) {
+              bu.setRepository(repo.getRepository(), rw, oi);
+              bu.setNotify(NotifyResolver.Result.none());
+              bu.addOp(c.getId(), inserter);
+              bu.execute();
+            }
+          });
 
       return inserter.getChange();
     }
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
index a5fd4a2..d2ccaa9 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionCheckTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePaths;
@@ -38,7 +39,7 @@
     GitRepositoryManager repoManager = new InMemoryRepositoryManager();
     repoManager.createRepository(allProjectsName);
     versionManager = new NoteDbSchemaVersionManager(allProjectsName, repoManager);
-    versionManager.init();
+    testRefAction(() -> versionManager.init());
 
     sitePaths = new SitePaths(Paths.get("/tmp/foo"));
   }
@@ -51,7 +52,7 @@
 
   @Test
   public void shouldFailIfCurrentVersionIsOneMoreThanExpected() throws IOException {
-    versionManager.increment(NoteDbSchemaVersions.LATEST);
+    testRefAction(() -> versionManager.increment(NoteDbSchemaVersions.LATEST));
 
     ProvisionException e =
         assertThrows(
@@ -69,7 +70,7 @@
           throws IOException {
     Config gerritConfig = new Config();
     gerritConfig.setBoolean("gerrit", null, "experimentalRollingUpgrade", true);
-    versionManager.increment(NoteDbSchemaVersions.LATEST);
+    testRefAction(() -> versionManager.increment(NoteDbSchemaVersions.LATEST));
 
     NoteDbSchemaVersionCheck versionCheck =
         new NoteDbSchemaVersionCheck(versionManager, sitePaths, gerritConfig);
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
index 38e19f7..3a1ea12 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionManagerTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.entities.RefNames.REFS_VERSION;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -62,14 +63,14 @@
 
   @Test
   public void incrementFromMissing() throws Exception {
-    manager.increment(123);
+    testRefAction(() -> manager.increment(123));
     assertThat(manager.read()).isEqualTo(124);
   }
 
   @Test
   public void increment() throws Exception {
     tr.update(REFS_VERSION, tr.blob("123"));
-    manager.increment(123);
+    testRefAction(() -> manager.increment(123));
     assertThat(manager.read()).isEqualTo(124);
   }
 
diff --git a/javatests/com/google/gerrit/server/submit/BUILD b/javatests/com/google/gerrit/server/submit/BUILD
index 7425bc8..01acb72 100644
--- a/javatests/com/google/gerrit/server/submit/BUILD
+++ b/javatests/com/google/gerrit/server/submit/BUILD
@@ -11,6 +11,7 @@
         "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//lib:jgit",
         "//lib/mockito",
         "//lib/truth",
diff --git a/javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java b/javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java
index 313e697..a391c03 100644
--- a/javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java
+++ b/javatests/com/google/gerrit/server/submit/SubmoduleCommitsTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
 
@@ -197,7 +198,7 @@
     RefUpdate ru = serverRepo.updateRef(refName);
     ru.setExpectedOldObjectId(oldCommitId);
     ru.setNewObjectId(newCommitId);
-    assertThat(ru.update()).isEqualTo(RefUpdate.Result.FAST_FORWARD);
+    testRefAction(() -> assertThat(ru.update()).isEqualTo(RefUpdate.Result.FAST_FORWARD));
     return rw.parseCommit(newCommitId);
   }
 
diff --git a/javatests/com/google/gerrit/server/update/BUILD b/javatests/com/google/gerrit/server/update/BUILD
index 6d96c10..345681d 100644
--- a/javatests/com/google/gerrit/server/update/BUILD
+++ b/javatests/com/google/gerrit/server/update/BUILD
@@ -15,6 +15,7 @@
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/server/util/time",
         "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
         "//lib:guava",
         "//lib:jgit",
         "//lib:jgit-junit",
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 91c8371..07159b7 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -18,6 +18,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -48,6 +49,7 @@
 import com.google.gerrit.server.notedb.Sequences;
 import com.google.gerrit.server.patch.DiffSummary;
 import com.google.gerrit.server.patch.DiffSummaryKey;
+import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.InMemoryTestEnvironment;
 import com.google.inject.Inject;
@@ -58,6 +60,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -106,12 +109,21 @@
   private Project.NameKey project;
   private TestRepository<Repository> repo;
 
+  private RefUpdateContext testRefUpdateContext;
+
   @Before
   public void setUp() throws Exception {
     project = Project.nameKey("test");
 
     Repository inMemoryRepo = repoManager.createRepository(project);
     repo = new TestRepository<>(inMemoryRepo);
+    // All tests here are low level. Open context here to avoid repeated code in multiple tests.
+    testRefUpdateContext = openTestRefUpdateContext();
+  }
+
+  @After
+  public void tearDown() {
+    testRefUpdateContext.close();
   }
 
   @Test
@@ -129,7 +141,6 @@
           });
       bu.execute();
     }
-
     assertThat(repo.getRepository().exactRef("refs/heads/master").getObjectId())
         .isEqualTo(branchCommit.getId());
   }
@@ -341,7 +352,8 @@
 
     int cacheSizeBefore = diffSummaryCache.asMap().size();
 
-    // We don't want to depend on the test helper used above so we perform an explicit commit here.
+    // We don't want to depend on the test helper used above so we perform an explicit commit
+    // here.
     try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       ObjectId commitId =
           repo.amend(notes.getCurrentPatchSet().commitId())
diff --git a/javatests/com/google/gerrit/server/update/RepoViewTest.java b/javatests/com/google/gerrit/server/update/RepoViewTest.java
index b37e302..b118c9f 100644
--- a/javatests/com/google/gerrit/server/update/RepoViewTest.java
+++ b/javatests/com/google/gerrit/server/update/RepoViewTest.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 
 import com.google.gerrit.entities.Project;
@@ -43,8 +44,14 @@
     InMemoryRepositoryManager repoManager = new InMemoryRepositoryManager();
     Project.NameKey project = Project.nameKey("project");
     repo = repoManager.createRepository(project);
-    tr = new TestRepository<>(repo);
-    tr.branch(MASTER).commit().create();
+    tr =
+        testRefAction(
+            () -> {
+              TestRepository<?> testRepo = new TestRepository<>(repo);
+              testRepo.branch(MASTER).commit().create();
+              return testRepo;
+            });
+
     view = new RepoView(repoManager, project);
   }
 
@@ -75,8 +82,11 @@
     assertThat(view.getRef(MASTER)).hasValue(oldMaster);
     assertThat(view.getRef(BRANCH)).isEmpty();
 
-    tr.branch(MASTER).commit().create();
-    tr.branch(BRANCH).commit().create();
+    testRefAction(
+        () -> {
+          tr.branch(MASTER).commit().create();
+          tr.branch(BRANCH).commit().create();
+        });
     assertThat(repo.exactRef(MASTER).getObjectId()).isNotEqualTo(oldMaster);
     assertThat(repo.exactRef(BRANCH)).isNotNull();
     assertThat(view.getRef(MASTER)).hasValue(oldMaster);
@@ -88,7 +98,7 @@
     ObjectId oldMaster = repo.exactRef(MASTER).getObjectId();
     assertThat(view.getRefs(R_HEADS)).containsExactly("master", oldMaster);
 
-    ObjectId newBranch = tr.branch(BRANCH).commit().create();
+    ObjectId newBranch = testRefAction(() -> tr.branch(BRANCH).commit().create());
     assertThat(view.getRefs(R_HEADS)).containsExactly("master", oldMaster, "branch", newBranch);
   }
 
@@ -99,17 +109,17 @@
     assertThat(view.getRef(MASTER)).hasValue(master1);
 
     // Doesn't reflect new value for master.
-    ObjectId master2 = tr.branch(MASTER).commit().create();
+    ObjectId master2 = testRefAction(() -> tr.branch(MASTER).commit().create());
     assertThat(repo.exactRef(MASTER).getObjectId()).isEqualTo(master2);
     assertThat(view.getRefs(R_HEADS)).containsExactly("master", master1);
 
     // Branch wasn't previously cached, so does reflect new value.
-    ObjectId branch1 = tr.branch(BRANCH).commit().create();
+    ObjectId branch1 = testRefAction(() -> tr.branch(BRANCH).commit().create());
     assertThat(view.getRefs(R_HEADS)).containsExactly("master", master1, "branch", branch1);
 
     // Looking up branch causes it to be cached.
     assertThat(view.getRef(BRANCH)).hasValue(branch1);
-    ObjectId branch2 = tr.branch(BRANCH).commit().create();
+    ObjectId branch2 = testRefAction(() -> tr.branch(BRANCH).commit().create());
     assertThat(repo.exactRef(BRANCH).getObjectId()).isEqualTo(branch2);
     assertThat(view.getRefs(R_HEADS)).containsExactly("master", master1, "branch", branch1);
   }
diff --git a/javatests/com/google/gerrit/server/update/context/BUILD b/javatests/com/google/gerrit/server/update/context/BUILD
new file mode 100644
index 0000000..e580595
--- /dev/null
+++ b/javatests/com/google/gerrit/server/update/context/BUILD
@@ -0,0 +1,14 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+    name = "update_context_tests",
+    size = "small",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//java/com/google/gerrit/server",
+        "//java/com/google/gerrit/testing:gerrit-test-util",
+        "//java/com/google/gerrit/testing:test-ref-update-context",
+        "//lib/truth",
+        "//lib/truth:truth-java8-extension",
+    ],
+)
diff --git a/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java b/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java
new file mode 100644
index 0000000..178d67d
--- /dev/null
+++ b/javatests/com/google/gerrit/server/update/context/RefUpdateContextTest.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2023 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.context;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GROUPS_UPDATE;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.INIT_REPO;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.After;
+import org.junit.Test;
+
+public class RefUpdateContextTest {
+  @After
+  public void tearDown() {
+    // Each test should close all opened context to avoid interference with other tests.
+    assertThat(RefUpdateContext.getOpenedContexts()).isEmpty();
+  }
+
+  @Test
+  public void contextNotOpen() {
+    assertThat(RefUpdateContext.getOpenedContexts()).isEmpty();
+    assertThat(RefUpdateContext.hasOpen(INIT_REPO)).isFalse();
+    assertThat(RefUpdateContext.hasOpen(GROUPS_UPDATE)).isFalse();
+  }
+
+  @Test
+  public void singleContext_openedAndClosedCorrectly() {
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      ImmutableList<RefUpdateContext> openedContexts = RefUpdateContext.getOpenedContexts();
+      assertThat(openedContexts).hasSize(1);
+      assertThat(openedContexts.get(0).getUpdateType()).isEqualTo(CHANGE_MODIFICATION);
+      assertThat(RefUpdateContext.hasOpen(CHANGE_MODIFICATION)).isTrue();
+      assertThat(RefUpdateContext.hasOpen(INIT_REPO)).isFalse();
+    }
+
+    assertThat(RefUpdateContext.getOpenedContexts()).isEmpty();
+    assertThat(RefUpdateContext.hasOpen(CHANGE_MODIFICATION)).isFalse();
+  }
+
+  @Test
+  public void nestedContext_openedAndClosedCorrectly() {
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (RefUpdateContext nestedCtx = RefUpdateContext.open(INIT_REPO)) {
+        ImmutableList<RefUpdateContext> nestedOpenedContexts = RefUpdateContext.getOpenedContexts();
+        assertThat(nestedOpenedContexts).hasSize(2);
+        assertThat(nestedOpenedContexts.get(0).getUpdateType()).isEqualTo(CHANGE_MODIFICATION);
+        assertThat(nestedOpenedContexts.get(1).getUpdateType()).isEqualTo(INIT_REPO);
+        assertThat(RefUpdateContext.hasOpen(CHANGE_MODIFICATION)).isTrue();
+        assertThat(RefUpdateContext.hasOpen(INIT_REPO)).isTrue();
+        assertThat(RefUpdateContext.hasOpen(GROUPS_UPDATE)).isFalse();
+      }
+      ImmutableList<RefUpdateContext> openedContexts = RefUpdateContext.getOpenedContexts();
+      assertThat(openedContexts).hasSize(1);
+      assertThat(openedContexts.get(0).getUpdateType()).isEqualTo(CHANGE_MODIFICATION);
+      assertThat(RefUpdateContext.hasOpen(CHANGE_MODIFICATION)).isTrue();
+      assertThat(RefUpdateContext.hasOpen(INIT_REPO)).isFalse();
+      assertThat(RefUpdateContext.hasOpen(GROUPS_UPDATE)).isFalse();
+    }
+
+    assertThat(RefUpdateContext.getOpenedContexts()).isEmpty();
+    assertThat(RefUpdateContext.hasOpen(CHANGE_MODIFICATION)).isFalse();
+    assertThat(RefUpdateContext.hasOpen(INIT_REPO)).isFalse();
+    assertThat(RefUpdateContext.hasOpen(GROUPS_UPDATE)).isFalse();
+  }
+
+  @Test
+  public void incorrectCloseOrder_exceptionThrown() {
+    try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+      try (RefUpdateContext nestedCtx = RefUpdateContext.open(INIT_REPO)) {
+        assertThrows(Exception.class, () -> ctx.close());
+        ImmutableList<RefUpdateContext> openedContexts = RefUpdateContext.getOpenedContexts();
+        assertThat(openedContexts).hasSize(2);
+        assertThat(RefUpdateContext.hasOpen(CHANGE_MODIFICATION)).isTrue();
+        assertThat(RefUpdateContext.hasOpen(INIT_REPO)).isTrue();
+      }
+    }
+  }
+}
diff --git a/lib/bouncycastle/BUILD b/lib/bouncycastle/BUILD
index 43ba6e1..6a87d73 100644
--- a/lib/bouncycastle/BUILD
+++ b/lib/bouncycastle/BUILD
@@ -22,6 +22,13 @@
 )
 
 java_library(
+    name = "bcutil",
+    data = ["//lib:LICENSE-bouncycastle"],
+    visibility = ["//visibility:public"],
+    exports = ["@bcutil//jar"],
+)
+
+java_library(
     name = "bcprov-neverlink",
     data = ["//lib:LICENSE-bouncycastle"],
     neverlink = 1,
@@ -44,3 +51,11 @@
     visibility = ["//visibility:public"],
     exports = ["@bcpkix//jar"],
 )
+
+java_library(
+    name = "bcutil-neverlink",
+    data = ["//lib:LICENSE-bouncycastle"],
+    neverlink = 1,
+    visibility = ["//visibility:public"],
+    exports = ["@bcutil//jar"],
+)
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index f915432..b9ed56b 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -258,6 +258,8 @@
   NONE = 'NONE',
 }
 
+// These defaults should match the defaults in
+// java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
 export function createDefaultPreferences(): PreferencesInfo {
   return {
     changes_per_page: 25,
@@ -265,8 +267,8 @@
     size_bar_in_change_table: true,
     my: [],
     theme: AppTheme.AUTO,
-    date_format: DateFormat.EURO,
-    time_format: TimeFormat.HHMM_24,
+    date_format: DateFormat.STD,
+    time_format: TimeFormat.HHMM_12,
     change_table: [],
     email_strategy: EmailStrategy.ATTENTION_SET_ONLY,
     default_base_for_merges: DefaultBase.AUTO_MERGE,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
index 342b876..19207bc 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.ts
@@ -42,6 +42,7 @@
 import {classMap} from 'lit/directives/class-map.js';
 import {createSearchUrl} from '../../../models/views/search';
 import {createChangeUrl} from '../../../models/views/change';
+import {userModelToken} from '../../../models/user/user-model';
 import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
 
 enum ChangeSize {
@@ -94,9 +95,6 @@
   sectionName?: string;
 
   @property({type: Boolean})
-  showStar = false;
-
-  @property({type: Boolean})
   showNumber = false;
 
   @property({type: String})
@@ -125,6 +123,10 @@
 
   private readonly getNavigation = resolve(this, navigationToken);
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
+  @state() private isLoggedIn = false;
+
   constructor() {
     super();
     subscribe(
@@ -134,6 +136,11 @@
         this.updateCheckedState(selectedChangeNums);
       }
     );
+    subscribe(
+      this,
+      () => this.getUserModel().loggedIn$,
+      isLoggedIn => (this.isLoggedIn = isLoggedIn)
+    );
   }
 
   override connectedCallback() {
@@ -332,6 +339,8 @@
   }
 
   private renderCellSelectionBox() {
+    if (!this.isLoggedIn) return;
+
     return html`
       <td class="cell selection">
         <!--
@@ -352,7 +361,7 @@
   }
 
   private renderCellStar() {
-    if (!this.showStar) return;
+    if (!this.isLoggedIn) return;
 
     return html`
       <td class="cell star">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
index c7cb5b8..5e31cc8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.ts
@@ -8,9 +8,11 @@
 import {
   SubmitRequirementResultInfo,
   NumericChangeId,
+  Timestamp,
 } from '../../../api/rest-api';
 import '../../../test/common-test-setup';
 import {
+  createAccountWithEmail,
   createAccountWithId,
   createChange,
   createSubmitRequirementExpressionInfo,
@@ -21,7 +23,6 @@
 import {
   query,
   queryAndAssert,
-  stubRestApi,
   waitUntilObserved,
 } from '../../../test/test-utils';
 import {
@@ -43,6 +44,7 @@
   bulkActionsModelToken,
   BulkActionsModel,
 } from '../../../models/bulk-actions/bulk-actions-model';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
 import {createTestAppContext} from '../../../test/test-app-context-init';
 import {ColumnNames} from '../../../constants/constants';
 import {testResolver} from '../../../test/common-test-setup';
@@ -58,13 +60,13 @@
 
   let element: GrChangeListItem;
   let bulkActionsModel: BulkActionsModel;
+  let userModel: UserModel;
 
   setup(async () => {
-    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
-
     bulkActionsModel = new BulkActionsModel(
       createTestAppContext().restApiService
     );
+    userModel = testResolver(userModelToken);
     element = (
       await fixture<DIProviderElement>(
         wrapInProvider(
@@ -105,6 +107,10 @@
     test('bulk actions checkboxes', async () => {
       element.change = {...createChange(), _number: 1 as NumericChangeId};
       bulkActionsModel.sync([element.change]);
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       await element.updateComplete;
 
       const checkbox = queryAndAssert<HTMLInputElement>(
@@ -134,6 +140,10 @@
       element.globalIndex = 5;
       element.change = {...createChange(), _number: 1 as NumericChangeId};
       bulkActionsModel.sync([element.change]);
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       await element.updateComplete;
 
       const checkbox = queryAndAssert<HTMLInputElement>(
@@ -147,6 +157,10 @@
     });
 
     test('checkbox state updates with model updates', async () => {
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       element.requestUpdate();
       await element.updateComplete;
 
@@ -168,6 +182,10 @@
     });
 
     test('checkbox state updates with change id update', async () => {
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       element.requestUpdate();
       await element.updateComplete;
 
@@ -361,7 +379,10 @@
     const change = createChange();
     bulkActionsModel.sync([change]);
     bulkActionsModel.addSelectedChangeNum(change._number);
-    element.showStar = true;
+    userModel.setAccount({
+      ...createAccountWithEmail('abc@def.com'),
+      registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+    });
     element.showNumber = true;
     element.account = createAccountWithId(1);
     element.config = createServerInfo();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
index 8227e11..61b276e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts
@@ -15,14 +15,15 @@
 import {sharedStyles} from '../../../styles/shared-styles';
 import {Metadata} from '../../../utils/change-metadata-util';
 import {WAITING} from '../../../constants/constants';
-import {provide} from '../../../models/dependency';
+import {provide, resolve} from '../../../models/dependency';
 import {
   bulkActionsModelToken,
   BulkActionsModel,
 } from '../../../models/bulk-actions/bulk-actions-model';
+import {createSearchUrl} from '../../../models/views/search';
+import {userModelToken} from '../../../models/user/user-model';
 import {subscribe} from '../../lit/subscription-controller';
 import {classMap} from 'lit/directives/class-map.js';
-import {createSearchUrl} from '../../../models/views/search';
 
 const NUMBER_FIXED_COLUMNS = 4;
 const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
@@ -52,9 +53,6 @@
   visibleChangeTableColumns?: string[];
 
   @property({type: Boolean})
-  showStar = false;
-
-  @property({type: Boolean})
   showNumber?: boolean; // No default value to prevent flickering.
 
   @property({type: Number})
@@ -104,6 +102,10 @@
     getAppContext().restApiService
   );
 
+  private readonly getUserModel = resolve(this, userModelToken);
+
+  private isLoggedIn = false;
+
   static override get styles() {
     return [
       changeListStyles,
@@ -156,6 +158,11 @@
       () => this.bulkActionsModel.totalChangeCount$,
       totalChangeCount => (this.totalChangeCount = totalChangeCount)
     );
+    subscribe(
+      this,
+      () => this.getUserModel().loggedIn$,
+      isLoggedIn => (this.isLoggedIn = isLoggedIn)
+    );
   }
 
   override willUpdate(changedProperties: PropertyValues) {
@@ -189,8 +196,8 @@
         <td class="leftPadding" aria-hidden="true"></td>
         <td
           class="star"
-          ?aria-hidden=${!this.showStar}
-          ?hidden=${!this.showStar}
+          ?aria-hidden=${!this.isLoggedIn}
+          ?hidden=${!this.isLoggedIn}
         ></td>
         <td class="cell" colspan=${colSpan}>
           ${this.changeSection.emptyStateSlotName
@@ -213,7 +220,7 @@
       <tbody>
         <tr class="groupHeader">
           <td aria-hidden="true" class="leftPadding"></td>
-          <td aria-hidden="true" class="star" ?hidden=${!this.showStar}></td>
+          <td aria-hidden="true" class="star" ?hidden=${!this.isLoggedIn}></td>
           <td class="cell" colspan=${colSpan}>
             <h2 class="heading-3">
               <a
@@ -248,7 +255,7 @@
           : html` <td
                 class="star"
                 aria-label="Star status column"
-                ?hidden=${!this.showStar}
+                ?hidden=${!this.isLoggedIn}
               ></td>
               <td class="number" ?hidden=${!this.showNumber}>#</td>
               ${columns.map(item => this.renderHeaderCell(item))}
@@ -267,7 +274,7 @@
     const indeterminate =
       this.numSelected > 0 && this.numSelected !== this.totalChangeCount;
     return html`
-      <td class="selection">
+      <td class="selection" ?hidden=${!this.isLoggedIn}>
         <!--
           The .checked property must be used rather than the attribute because
           the attribute only controls the default checked state and does not
@@ -322,7 +329,6 @@
         .sectionName=${this.changeSection.name}
         .visibleChangeTableColumns=${columns}
         .showNumber=${this.showNumber}
-        ?showStar=${this.showStar}
         .usp=${this.usp}
         .labelNames=${this.labelNames}
         .globalIndex=${this.startIndex + index}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
index 8dfecdc..63552c7 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section_test.ts
@@ -13,9 +13,10 @@
 import {
   createChange,
   createAccountDetailWithId,
+  createAccountWithEmail,
   createServerInfo,
 } from '../../../test/test-data-generators';
-import {NumericChangeId, ChangeInfoId} from '../../../api/rest-api';
+import {ChangeInfoId, NumericChangeId, Timestamp} from '../../../api/rest-api';
 import {
   queryAll,
   query,
@@ -27,11 +28,15 @@
 import {ChangeListSection} from '../gr-change-list/gr-change-list';
 import {fixture, html, assert} from '@open-wc/testing';
 import {ColumnNames} from '../../../constants/constants';
+import {testResolver} from '../../../test/common-test-setup';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
 
 suite('gr-change-list section', () => {
   let element: GrChangeListSection;
+  let userModel: UserModel;
 
   setup(async () => {
+    userModel = testResolver(userModelToken);
     const changeSection: ChangeListSection = {
       name: 'test',
       query: 'test',
@@ -193,6 +198,10 @@
         ],
         emptyStateSlotName: 'test',
       };
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       await element.updateComplete;
       let rows = queryAll(element, 'gr-change-list-item');
       assert.lengthOf(rows, 2);
@@ -235,6 +244,10 @@
         ],
         emptyStateSlotName: 'test',
       };
+      userModel.setAccount({
+        ...createAccountWithEmail('abc@def.com'),
+        registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+      });
       await element.updateComplete;
       const rows = queryAll(element, 'gr-change-list-item');
 
@@ -273,6 +286,31 @@
     });
   });
 
+  test('no checkbox when logged out', async () => {
+    element.changeSection = {
+      name: 'test',
+      query: 'test',
+      results: [
+        {
+          ...createChange(),
+          _number: 1 as NumericChangeId,
+          id: '1' as ChangeInfoId,
+        },
+        {
+          ...createChange(),
+          _number: 2 as NumericChangeId,
+          id: '2' as ChangeInfoId,
+        },
+      ],
+      emptyStateSlotName: 'test',
+    };
+    userModel.setAccount(undefined);
+    await element.updateComplete;
+    const rows = queryAll(element, 'gr-change-list-item');
+    assert.lengthOf(rows, 2);
+    assert.isUndefined(query<HTMLInputElement>(rows[0], 'input'));
+  });
+
   test('colspans', async () => {
     element.visibleChangeTableColumns = [];
     element.changeSection = {results: [{...createChange()}]};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
index d2ba2c9..1c86354 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.ts
@@ -195,7 +195,6 @@
           .account=${this.account}
           .changes=${this.changes}
           .preferences=${this.preferences}
-          .showStar=${this.loggedIn}
           @toggle-star=${(e: CustomEvent<ChangeStarToggleStarDetail>) => {
             this.handleToggleStar(e);
           }}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
index 4c43da5..748c2b8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.ts
@@ -113,9 +113,6 @@
   showNumber?: boolean; // No default value to prevent flickering.
 
   @property({type: Boolean})
-  showStar = false;
-
-  @property({type: Boolean})
   showReviewedState = false;
 
   @property({type: Array})
@@ -270,7 +267,6 @@
           sectionIndex,
           this.sections
         )}
-        ?showStar=${this.showStar}
         .showNumber=${this.showNumber}
         .visibleChangeTableColumns=${this.visibleChangeTableColumns}
         .usp=${this.usp}
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
index bef3166..7e9735f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.ts
@@ -23,6 +23,7 @@
 } from '../../../constants/constants';
 import {AccountId, NumericChangeId} from '../../../types/common';
 import {
+  createAccountWithEmail,
   createChange,
   createServerInfo,
   createSubmitRequirementResultInfo,
@@ -32,12 +33,16 @@
 import {fixture, assert} from '@open-wc/testing';
 import {html} from 'lit';
 import {testResolver} from '../../../test/common-test-setup';
+import {Timestamp} from '../../../api/rest-api';
+import {UserModel, userModelToken} from '../../../models/user/user-model';
 
 suite('gr-change-list basic tests', () => {
   let element: GrChangeList;
+  let userModel: UserModel;
 
   setup(async () => {
     element = await fixture(html`<gr-change-list></gr-change-list>`);
+    userModel = testResolver(userModelToken);
   });
 
   test('renders', async () => {
@@ -285,6 +290,12 @@
   });
 
   test('toggle checkbox keyboard shortcut', async () => {
+    userModel.setAccount({
+      ...createAccountWithEmail('abc@def.com'),
+      registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+    });
+    await element.updateComplete;
+
     const getCheckbox = (item: GrChangeListItem) =>
       queryAndAssert<HTMLInputElement>(query(item, '.selection'), 'input');
 
@@ -515,7 +526,7 @@
     assert.isTrue(element.isColumnEnabled('Subject'));
   });
 
-  test('showStar and showNumber', async () => {
+  test('loggedIn and showNumber', async () => {
     element.sections = [{results: [{...createChange()}], name: 'a'}];
     element.account = {_account_id: 1001 as AccountId};
     element.preferences = {
@@ -534,6 +545,7 @@
       ],
     };
     element.config = createServerInfo();
+    userModel.setAccount(undefined);
     await element.updateComplete;
     const section = query<GrChangeListSection>(
       element,
@@ -547,7 +559,10 @@
     assert.isNotOk(query(query(section, 'gr-change-list-item'), '.star'));
     assert.isNotOk(query(query(section, 'gr-change-list-item'), '.number'));
 
-    element.showStar = true;
+    userModel.setAccount({
+      ...createAccountWithEmail('abc@def.com'),
+      registered_on: '2015-03-12 18:32:08.000000000' as Timestamp,
+    });
     await element.updateComplete;
     await section.updateComplete;
     assert.isOk(query(query(section, 'gr-change-list-item'), '.star'));
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
index d013654..cd30440 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.ts
@@ -284,7 +284,6 @@
         ${this.renderUserHeader()}
         <h1 class="assistive-tech-only">Dashboard</h1>
         <gr-change-list
-          ?showStar=${true}
           .account=${this.account}
           .preferences=${this.preferences}
           .sections=${this.results}
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
index 17d7e95..84a3139 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.ts
@@ -82,7 +82,7 @@
         <div class="loading" hidden="">Loading...</div>
         <div>
           <h1 class="assistive-tech-only">Dashboard</h1>
-          <gr-change-list showstar="">
+          <gr-change-list>
             <div id="emptyOutgoing" slot="outgoing-slot">No changes</div>
             <div id="emptyYourTurn" slot="your-turn-slot">
               <span> No changes need your attention &nbsp🎉 </span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 72a6a3a..721d650 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -115,7 +115,6 @@
 import {GrFileList} from '../gr-file-list/gr-file-list';
 import {EditRevisionInfo, ParsedChangeInfo} from '../../../types/types';
 import {
-  CloseFixPreviewEvent,
   EditableContentSaveEvent,
   EventType,
   OpenFixPreviewEvent,
@@ -590,8 +589,9 @@
     this.addEventListener('editable-content-cancel', () =>
       this.handleCommitMessageCancel()
     );
-    this.addEventListener('open-fix-preview', e => this.onOpenFixPreview(e));
-    this.addEventListener('close-fix-preview', e => this.onCloseFixPreview(e));
+    this.addEventListener(EventType.OPEN_FIX_PREVIEW, e =>
+      this.onOpenFixPreview(e)
+    );
 
     this.addEventListener(EventType.SHOW_TAB, e => this.setActiveTab(e));
     this.addEventListener('reload', e => {
@@ -1675,10 +1675,6 @@
     this.applyFixDialog.open(e);
   }
 
-  private onCloseFixPreview(e: CloseFixPreviewEvent) {
-    if (e.detail.fixApplied) fireReload(this);
-  }
-
   // Private but used in tests.
   handleToggleDiffMode() {
     if (this.diffViewMode === DiffViewMode.SIDE_BY_SIDE) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 6f8bd9a..c2739f3 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -218,7 +218,6 @@
             <gr-autocomplete
               id="parentInput"
               .query=${this.query}
-              no-debounce
               .text=${this.text}
               @text-changed=${(e: ValueChangedEvent) =>
                 (this.text = e.detail.value)}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index 776e923..2644d81 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -74,7 +74,6 @@
             <gr-autocomplete
               allow-non-suggested-values=""
               id="parentInput"
-              no-debounce=""
               placeholder="Change number, ref, or commit hash"
             >
             </gr-autocomplete>
@@ -305,7 +304,6 @@
 
     test('input text change triggers function', async () => {
       const recentChangesSpy = sinon.spy(element, 'getRecentChanges');
-      element.parentInput.noDebounce = true;
       pressKey(
         queryAndAssert(queryAndAssert(element, '#parentInput'), '#input'),
         Key.ENTER
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index d4defcb..6eaf7ae 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -258,10 +258,6 @@
 
   // Private but used in tests.
   @state()
-  displayLine?: boolean;
-
-  // Private but used in tests.
-  @state()
   showSizeBars = true;
 
   // For merge commits vs Auto Merge, an extra file row is shown detailing the
@@ -733,9 +729,6 @@
     this.shortcutsController.addAbstract(Shortcut.TOGGLE_LEFT_PANE, _ =>
       this.handleToggleLeftPane()
     );
-    this.shortcutsController.addGlobal({key: Key.ESC}, _ =>
-      this.handleEscKey()
-    );
     this.shortcutsController.addAbstract(
       Shortcut.EXPAND_ALL_COMMENT_THREADS,
       _ => {}
@@ -1079,7 +1072,6 @@
           <gr-diff-host
             ?noAutoRender=${true}
             ?showLoadFailure=${true}
-            .displayLine=${this.displayLine}
             .changeNum=${this.changeNum}
             .change=${this.change}
             .patchRange=${this.patchRange}
@@ -2032,7 +2024,6 @@
     e.stopPropagation();
     if (this.filesExpanded === FilesExpandedState.ALL) {
       this.diffCursor?.moveDown();
-      this.displayLine = true;
     } else {
       this.fileCursor.next({circular: true});
       this.selectedIndex = this.fileCursor.index;
@@ -2052,7 +2043,6 @@
     e.stopPropagation();
     if (this.filesExpanded === FilesExpandedState.ALL) {
       this.diffCursor?.moveUp();
-      this.displayLine = true;
     } else {
       this.fileCursor.previous({circular: true});
       this.selectedIndex = this.fileCursor.index;
@@ -2504,11 +2494,6 @@
     return undefined;
   }
 
-  // Private but used in tests.
-  handleEscKey() {
-    this.displayLine = false;
-  }
-
   /**
    * Compute size bar layout values from the file list.
    * Private but used in tests.
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
index 1fbddc5..c79be96 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.ts
@@ -83,9 +83,6 @@
       stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
       stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
-      stubElement('gr-date-formatter', 'loadTimeFormat').callsFake(() =>
-        Promise.resolve()
-      );
       stubElement('gr-diff-host', 'reload').callsFake(() => Promise.resolve());
       stubElement('gr-diff-host', 'prefetchDiff').callsFake(() => {});
 
@@ -2071,9 +2068,6 @@
       stubRestApi('getDiffComments').returns(Promise.resolve({}));
       stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
       stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
-      stubElement('gr-date-formatter', 'loadTimeFormat').callsFake(() =>
-        Promise.resolve()
-      );
       stubRestApi('getDiff').callsFake(() => Promise.resolve(createDiff()));
       stubElement('gr-diff-host', 'prefetchDiff').callsFake(() => {});
 
@@ -2305,22 +2299,6 @@
       assert.isTrue(setUrlStub.calledOnce);
     });
 
-    test('displayLine', () => {
-      element.filesExpanded = FilesExpandedState.ALL;
-
-      element.displayLine = false;
-      element.handleCursorNext(new KeyboardEvent('keydown'));
-      assert.isTrue(element.displayLine);
-
-      element.displayLine = false;
-      element.handleCursorPrev(new KeyboardEvent('keydown'));
-      assert.isTrue(element.displayLine);
-
-      element.displayLine = true;
-      element.handleEscKey();
-      assert.isFalse(element.displayLine);
-    });
-
     suite('editMode behavior', () => {
       test('reviewed checkbox', async () => {
         reviewFileStub.restore();
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
index 3af8207..1d2a272 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
@@ -280,6 +280,7 @@
 export class PageContext {
   /**
    * Includes everything: base, path, query and hash.
+   * NOT decoded.
    */
   canonicalPath = '';
 
@@ -287,18 +288,21 @@
    * Does not include base path.
    * Does not include hash.
    * Includes query string.
+   * NOT decoded.
    */
   path = '';
 
-  /** Does not include hash. */
+  /** Decoded. Does not include hash. */
   querystring = '';
 
+  /** Decoded. */
   hash = '';
 
   /**
    * Regular expression matches of capturing groups. The first entry params[0]
    * corresponds to the first capturing group. The entire matched string is not
    * returned in this array.
+   * Each param is double decoded.
    */
   params: string[] = [];
 
@@ -346,17 +350,24 @@
   replaceState() {
     window.history.replaceState(this.state, this.title, this.canonicalPath);
   }
+
+  match(re: RegExp) {
+    const qsIndex = this.path.indexOf('?');
+    const pathname = qsIndex !== -1 ? this.path.slice(0, qsIndex) : this.path;
+    const matches = re.exec(decodeURIComponent(pathname));
+    if (matches) {
+      this.params = matches
+        .slice(1)
+        .map(match => decodeURIComponentString(match));
+    }
+    return !!matches;
+  }
 }
 
 function createRoute(re: RegExp, fn: Function) {
   return (ctx: PageContext, next: Function) => {
-    const qsIndex = ctx.path.indexOf('?');
-    const pathname = qsIndex !== -1 ? ctx.path.slice(0, qsIndex) : ctx.path;
-    const matches = re.exec(decodeURIComponent(pathname));
+    const matches = ctx.match(re);
     if (matches) {
-      ctx.params = matches
-        .slice(1)
-        .map(match => decodeURIComponentString(match));
       fn(ctx, next);
     } else {
       next();
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 997d9d5..fe90471 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -14,7 +14,6 @@
 import {assertIsDefined} from '../../../utils/common-util';
 import {
   BasePatchSetNum,
-  DashboardId,
   GroupId,
   NumericChangeId,
   RevisionPatchSetNum,
@@ -73,6 +72,7 @@
 import {
   DashboardViewModel,
   DashboardViewState,
+  PROJECT_DASHBOARD_ROUTE,
 } from '../../../models/views/dashboard';
 import {
   SettingsViewModel,
@@ -107,7 +107,6 @@
 
   DASHBOARD: /^\/dashboard\/(.+)$/,
   CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
-  PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
   LEGACY_PROJECT_DASHBOARD: /^\/projects\/(.+),dashboards\/(.+)/,
 
   AGREEMENTS: /^\/settings\/agreements\/?/,
@@ -167,17 +166,12 @@
 
   PLUGINS: /^\/plugins\/(.+)$/,
 
-  // TODO: The first capturing group in the next 3 patterns works around a bug
-  // in page.js that is fixed in version 1.11.6. Remove it when the new version
-  // is being used at Google.
-  // https://github.com/visionmedia/page.js/commit/60f764b0ca9ad55133bc373914e97a8927a8f2d5
-
   // Matches /admin/plugins with optional filter and offset.
-  PLUGIN_LIST: /^(\/admin\/plugins)\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
+  PLUGIN_LIST: /^\/admin\/plugins\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
   // Matches /admin/groups with optional filter and offset.
-  GROUP_LIST: /^(\/admin\/groups)\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
+  GROUP_LIST: /^\/admin\/groups\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
   // Matches /admin/repos with optional filter and offset.
-  REPO_LIST: /^(\/admin\/repos)\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
+  REPO_LIST: /^\/admin\/repos\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
   // Matches /admin/repos/$REPO,branches with optional filter and offset.
   BRANCH_LIST:
     /^\/admin\/repos\/(.+),branches\/?(?:\/q\/filter:(.*?))?(?:,(\d+))?$/,
@@ -635,10 +629,10 @@
       ctx => this.handleCustomDashboardRoute(ctx)
     );
 
-    this.mapRoute(
-      RoutePattern.PROJECT_DASHBOARD,
-      'handleProjectDashboardRoute',
-      ctx => this.handleProjectDashboardRoute(ctx)
+    this.mapRouteState(
+      PROJECT_DASHBOARD_ROUTE,
+      this.dashboardViewModel,
+      'handleProjectDashboardRoute'
     );
 
     this.mapRoute(
@@ -1008,19 +1002,6 @@
     return Promise.resolve();
   }
 
-  handleProjectDashboardRoute(ctx: PageContext) {
-    const project = ctx.params[0] as RepoName;
-    const state: DashboardViewState = {
-      view: GerritView.DASHBOARD,
-      project,
-      dashboard: decodeURIComponent(ctx.params[1]) as DashboardId,
-    };
-    // Note that router model view must be updated before view models.
-    this.setState(state);
-    this.dashboardViewModel.setState(state);
-    this.reporting.setRepoName(project);
-  }
-
   handleLegacyProjectDashboardRoute(ctx: PageContext) {
     this.redirect('/p/' + ctx.params[0] + '/+/dashboard/' + ctx.params[1]);
   }
@@ -1070,10 +1051,10 @@
     const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.GROUPS,
-      offset: ctx.params[2] || '0',
-      filter: ctx.params[1] ?? null,
+      offset: ctx.params[1] || '0',
+      filter: ctx.params[0] ?? null,
       openCreateModal:
-        !ctx.params[1] && !ctx.params[2] && ctx.hash === 'create',
+        !ctx.params[0] && !ctx.params[1] && ctx.hash === 'create',
     };
     // Note that router model view must be updated before view models.
     this.setState(state);
@@ -1192,10 +1173,10 @@
     const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.REPOS,
-      offset: ctx.params[2] || '0',
-      filter: ctx.params[1] ?? null,
+      offset: ctx.params[1] || '0',
+      filter: ctx.params[0] ?? null,
       openCreateModal:
-        !ctx.params[1] && !ctx.params[2] && ctx.hash === 'create',
+        !ctx.params[0] && !ctx.params[1] && ctx.hash === 'create',
     };
     // Note that router model view must be updated before view models.
     this.setState(state);
@@ -1222,8 +1203,8 @@
     const state: AdminViewState = {
       view: GerritView.ADMIN,
       adminView: AdminChildView.PLUGINS,
-      offset: ctx.params[2] || '0',
-      filter: ctx.params[1] ?? null,
+      offset: ctx.params[1] || '0',
+      filter: ctx.params[0] ?? null,
     };
     // Note that router model view must be updated before view models.
     this.setState(state);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 17edc19..314e126 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -198,6 +198,9 @@
     return [
       sharedStyles,
       css`
+        gr-icon.searchIcon {
+          margin: 0 var(--spacing-xs);
+        }
         form {
           display: flex;
         }
@@ -217,7 +220,6 @@
         <gr-autocomplete
           id="searchInput"
           .label=${this.label}
-          show-search-icon
           .text=${this.inputVal}
           .query=${this.query}
           allow-non-suggested-values
@@ -232,6 +234,7 @@
             this.handleSearchTextChanged(e);
           }}
         >
+          <gr-icon icon="search" class="searchIcon" slot="prefix"></gr-icon>
           <a
             class="help"
             slot="suffix"
@@ -275,7 +278,7 @@
     // fallback to gerrit's official doc
     let baseUrl =
       this.docsBaseUrl ||
-      'https://gerrit-review.googlesource.com/documentation/';
+      'https://gerrit-review.googlesource.com/Documentation/';
     if (baseUrl.endsWith('/')) {
       baseUrl = baseUrl.substring(0, baseUrl.length - 1);
     }
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
index 01e2fb6..0694453 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.ts
@@ -69,9 +69,9 @@
             allow-non-suggested-values=""
             id="searchInput"
             multi=""
-            show-search-icon=""
             tab-complete=""
           >
+            <gr-icon icon="search" class="searchIcon" slot="prefix"></gr-icon>
             <a
               class="help"
               href="https://mydocumentationurl.google.com/user-search.html"
@@ -319,7 +319,7 @@
       await element.updateComplete;
       assert.equal(
         element.computeHelpDocLink(),
-        'https://gerrit-review.googlesource.com/documentation/' +
+        'https://gerrit-review.googlesource.com/Documentation/' +
           'user-search.html'
       );
     });
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
index b146e93..1608c22 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.ts
@@ -20,7 +20,6 @@
 import {PROVIDED_FIX_ID} from '../../../utils/comment-util';
 import {OpenFixPreviewEvent} from '../../../types/events';
 import {getAppContext} from '../../../services/app-context';
-import {fireCloseFixPreview} from '../../../utils/event-util';
 import {DiffLayer, ParsedChangeInfo} from '../../../types/types';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {TokenHighlightLayer} from '../../../embed/diff/gr-diff-builder/token-highlight-layer';
@@ -37,6 +36,8 @@
 import {GrSyntaxLayerWorker} from '../../../embed/diff/gr-syntax-layer/gr-syntax-layer-worker';
 import {highlightServiceToken} from '../../../services/highlight/highlight-service';
 import {anyLineTooLong} from '../../../embed/diff/gr-diff/gr-diff-utils';
+import {changeModelToken} from '../../../models/change/change-model';
+import {fireReload} from '../../../utils/event-util';
 
 interface FilePreview {
   filepath: string;
@@ -90,12 +91,20 @@
   @state()
   diffPrefs?: DiffPreferencesInfo;
 
+  @state()
+  isOwner = false;
+
+  @state()
+  onCloseFixPreviewCallbacks: ((fixapplied: boolean) => void)[] = [];
+
   private readonly restApiService = getAppContext().restApiService;
 
   private readonly getUserModel = resolve(this, userModelToken);
 
   private readonly getNavigation = resolve(this, navigationToken);
 
+  private readonly getChangeModel = resolve(this, changeModelToken);
+
   private readonly syntaxLayer = new GrSyntaxLayerWorker(
     resolve(this, highlightServiceToken),
     () => getAppContext().reportingService
@@ -105,6 +114,11 @@
     super();
     subscribe(
       this,
+      () => this.getChangeModel().isOwner$,
+      x => (this.isOwner = x)
+    );
+    subscribe(
+      this,
       () => this.getUserModel().preferences$,
       preferences => {
         const layers: DiffLayer[] = [this.syntaxLayer];
@@ -234,6 +248,7 @@
   open(e: OpenFixPreviewEvent) {
     this.patchNum = e.detail.patchNum;
     this.fixSuggestions = e.detail.fixSuggestions;
+    this.onCloseFixPreviewCallbacks = e.detail.onCloseFixPreviewCallbacks;
     assert(this.fixSuggestions.length > 0, 'no fix in the event');
     this.selectedFixIdx = 0;
     this.applyFixModal?.showModal();
@@ -319,12 +334,14 @@
     this.currentPreviews = [];
     this.isApplyFixLoading = false;
 
-    fireCloseFixPreview(this, fixApplied);
+    this.onCloseFixPreviewCallbacks.forEach(fn => fn(fixApplied));
     this.applyFixModal?.close();
+    if (fixApplied) fireReload(this);
   }
 
   private computeTooltip() {
     if (!this.change || !this.patchNum) return '';
+    if (!this.isOwner) return 'Fix can only be applied by author';
     const latestPatchNum =
       this.change.revisions[this.change.current_revision]._number;
     return latestPatchNum !== this.patchNum
@@ -334,6 +351,7 @@
 
   private computeDisableApplyFixButton() {
     if (!this.change || !this.patchNum) return true;
+    if (!this.isOwner) return true;
     const latestPatchNum =
       this.change.revisions[this.change.current_revision]._number;
     return this.patchNum !== latestPatchNum || this.isApplyFixLoading;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
index 24dadf7..9284fb2 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.ts
@@ -17,11 +17,7 @@
 } from '../../../test/test-data-generators';
 import {createDefaultDiffPrefs} from '../../../constants/constants';
 import {DiffInfo} from '../../../types/diff';
-import {
-  CloseFixPreviewEventDetail,
-  EventType,
-  OpenFixPreviewEventDetail,
-} from '../../../types/events';
+import {EventType, OpenFixPreviewEventDetail} from '../../../types/events';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {fixture, html, assert} from '@open-wc/testing';
 import {SinonStub} from 'sinon';
@@ -37,11 +33,13 @@
       createFixSuggestionInfo('fix_1'),
       createFixSuggestionInfo('fix_2'),
     ],
+    onCloseFixPreviewCallbacks: [],
   };
 
   const ONE_FIX: OpenFixPreviewEventDetail = {
     patchNum: 2 as PatchSetNum,
     fixSuggestions: [createFixSuggestionInfo('fix_1')],
+    onCloseFixPreviewCallbacks: [],
   };
 
   function getConfirmButton(): GrButton {
@@ -73,6 +71,7 @@
     element.changeNum = change._number;
     element.patchNum = change.revisions[change.current_revision]._number;
     element.change = change;
+    element.isOwner = true;
     element.diffPrefs = {
       ...createDefaultDiffPrefs(),
       font_size: 12,
@@ -162,8 +161,22 @@
       assert.equal(button.getAttribute('title'), '');
     });
 
+    test('apply fix button is disabled for non-author', async () => {
+      element.isOwner = false;
+      await element.updateComplete;
+      await open(TWO_FIXES);
+      assert.equal(element.currentFix!.fix_id, 'fix_1');
+      assert.equal(element.currentPreviews.length, 2);
+      const button = getConfirmButton();
+      assert.isTrue(button.hasAttribute('disabled'));
+      assert.equal(
+        button.getAttribute('title'),
+        'Fix can only be applied by author'
+      );
+    });
+
     test('apply fix button is disabled on older patchset', async () => {
-      element.change = element.change = {
+      element.change = {
         ...createParsedChange(),
         revisions: createRevisions(2),
         current_revision: getCurrentRevision(0),
@@ -246,11 +259,7 @@
     element.currentFix = createFixSuggestionInfo('123');
 
     const closeFixPreviewEventSpy = sinon.spy();
-    // Element is recreated after each test, removeEventListener isn't required
-    element.addEventListener(
-      EventType.CLOSE_FIX_PREVIEW,
-      closeFixPreviewEventSpy
-    );
+    element.onCloseFixPreviewCallbacks.push(closeFixPreviewEventSpy);
 
     await element.handleApplyFix(new CustomEvent('confirm'));
 
@@ -263,14 +272,7 @@
     assert.isTrue(setUrlStub.called);
     assert.equal(setUrlStub.lastCall.firstArg, '/c/test-project/+/42/2..edit');
 
-    sinon.assert.calledOnceWithExactly(
-      closeFixPreviewEventSpy,
-      new CustomEvent<CloseFixPreviewEventDetail>(EventType.CLOSE_FIX_PREVIEW, {
-        detail: {
-          fixApplied: true,
-        },
-      })
-    );
+    sinon.assert.calledOnceWithExactly(closeFixPreviewEventSpy, true);
     // reset gr-apply-fix-dialog and close
     assert.equal(element.currentFix, undefined);
     assert.equal(element.currentPreviews.length, 0);
@@ -311,11 +313,7 @@
     element.currentFix = createFixSuggestionInfo('fix_123');
 
     const closeFixPreviewEventSpy = sinon.spy();
-    // Element is recreated after each test, removeEventListener isn't required
-    element.addEventListener(
-      EventType.CLOSE_FIX_PREVIEW,
-      closeFixPreviewEventSpy
-    );
+    element.onCloseFixPreviewCallbacks.push(closeFixPreviewEventSpy);
 
     let expectedError;
     await element.handleApplyFix(new CustomEvent('click')).catch(e => {
@@ -328,19 +326,8 @@
 
   test('onCancel fires close with correct parameters', () => {
     const closeFixPreviewEventSpy = sinon.spy();
-    // Element is recreated after each test, removeEventListener isn't required
-    element.addEventListener(
-      EventType.CLOSE_FIX_PREVIEW,
-      closeFixPreviewEventSpy
-    );
+    element.onCloseFixPreviewCallbacks.push(closeFixPreviewEventSpy);
     element.onCancel(new CustomEvent('cancel'));
-    sinon.assert.calledOnceWithExactly(
-      closeFixPreviewEventSpy,
-      new CustomEvent<CloseFixPreviewEventDetail>(EventType.CLOSE_FIX_PREVIEW, {
-        detail: {
-          fixApplied: false,
-        },
-      })
-    );
+    sinon.assert.calledOnceWithExactly(closeFixPreviewEventSpy, false);
   });
 });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 23dce97..fd0e5d2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -171,9 +171,6 @@
   @property({type: String})
   projectName?: RepoName;
 
-  @property({type: Boolean})
-  displayLine = false;
-
   @state()
   private _isImageDiff = false;
 
@@ -522,7 +519,6 @@
       .noAutoRender=${this.noAutoRender}
       .path=${this.path}
       .prefs=${this.prefs}
-      .displayLine=${this.displayLine}
       .isImageDiff=${this.isImageDiff}
       .noRenderOnPrefsChange=${this.noRenderOnPrefsChange}
       .renderPrefs=${this.renderPrefs}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
index be03afd..7a32c27 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.ts
@@ -774,14 +774,6 @@
     assert.equal(element.diffElement.prefs, value);
   });
 
-  test('passes in displayLine', async () => {
-    const value = true;
-    element.displayLine = value;
-    await element.updateComplete;
-    assertIsDefined(element.diffElement);
-    assert.equal(element.diffElement.displayLine, value);
-  });
-
   test('passes in hidden', async () => {
     const value = true;
     element.hidden = value;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index c7d6948..19186ec 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -56,7 +56,7 @@
 import {OpenFixPreviewEvent, ValueChangedEvent} from '../../../types/events';
 import {fireAlert, fireEvent, fireTitleChange} from '../../../utils/event-util';
 import {assertIsDefined, queryAndAssert} from '../../../utils/common-util';
-import {Key, toggleClass, whenVisible} from '../../../utils/dom-util';
+import {toggleClass, whenVisible} from '../../../utils/dom-util';
 import {CursorMoveResult} from '../../../api/core';
 import {throttleWrap} from '../../../utils/async-util';
 import {filter, take, switchMap} from 'rxjs/operators';
@@ -344,10 +344,6 @@
     );
     listen(Shortcut.EXPAND_ALL_COMMENT_THREADS, _ => {}); // docOnly
     listen(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, _ => {}); // docOnly
-    this.shortcutsController.addGlobal({key: Key.ESC}, _ => {
-      assertIsDefined(this.diffHost, 'diffHost');
-      this.diffHost.displayLine = false;
-    });
   }
 
   private setupSubscriptions() {
@@ -1067,7 +1063,6 @@
 
   private handlePrevLine() {
     assertIsDefined(this.diffHost, 'diffHost');
-    this.diffHost.displayLine = true;
     this.cursor?.moveUp();
   }
 
@@ -1102,7 +1097,6 @@
 
   private handleNextLine() {
     assertIsDefined(this.diffHost, 'diffHost');
-    this.diffHost.displayLine = true;
     this.cursor?.moveDown();
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
index 889e9dd..6507046 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.ts
@@ -61,7 +61,6 @@
 import {GrDiffModeSelector} from '../../../embed/diff/gr-diff-mode-selector/gr-diff-mode-selector';
 import {fixture, html, assert} from '@open-wc/testing';
 import {EventType} from '../../../types/events';
-import {Key} from '../../../utils/dom-util';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {testResolver} from '../../../test/common-test-setup';
 import {UserModel, userModelToken} from '../../../models/user/user-model';
@@ -459,15 +458,6 @@
         element.diffHost.diffElement.viewMode,
         DiffViewMode.SIDE_BY_SIDE
       );
-      assert.isTrue(element.diffHost.diffElement.displayLine);
-
-      pressKey(element, Key.ESC);
-      await element.updateComplete;
-      assert.equal(
-        element.diffHost.diffElement.viewMode,
-        DiffViewMode.SIDE_BY_SIDE
-      );
-      assert.isFalse(element.diffHost.diffElement.displayLine);
 
       const setReviewedStub = sinon.stub(element, 'setReviewed');
       const handleToggleSpy = sinon.spy(element, 'handleToggleFileReviewed');
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
index 31283ad..0729f21 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.ts
@@ -217,16 +217,20 @@
       assert.isFalse(hideDialogStub.called);
       queryAndAssert<GrButton>(element, '#open').click();
       element.patchNum = 1 as RevisionPatchSetNum;
-      await waitUntilVisible(element.modal!);
+      await showDialogSpy.lastCall.returnValue;
       assert.isTrue(hideDialogStub.called);
       assert.isTrue(element.openDialog!.disabled);
       assert.isFalse(queryStub.called);
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       openAutoComplete.focused = true;
-      openAutoComplete.noDebounce = true;
       openAutoComplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        openAutoComplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await openAutoComplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       await waitUntil(() => !element.openDialog!.disabled);
       queryAndAssert<GrButton>(
@@ -242,7 +246,6 @@
       queryAndAssert<GrButton>(element, '#open').click();
       await waitUntilVisible(element.modal!);
       assert.isTrue(element.openDialog!.disabled);
-      openAutoComplete.noDebounce = true;
       openAutoComplete.text = 'src/test.cpp';
       await element.updateComplete;
       await waitUntil(() => !element.openDialog!.disabled);
@@ -277,9 +280,13 @@
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       deleteAutocomplete.focused = true;
-      deleteAutocomplete.noDebounce = true;
       deleteAutocomplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        deleteAutocomplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await deleteAutocomplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       await waitUntil(() => !element.deleteDialog!.disabled);
       queryAndAssert<GrButton>(
@@ -304,9 +311,13 @@
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       deleteAutocomplete.focused = true;
-      deleteAutocomplete.noDebounce = true;
       deleteAutocomplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        deleteAutocomplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await deleteAutocomplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       await waitUntil(() => !element.deleteDialog!.disabled);
       queryAndAssert<GrButton>(
@@ -363,9 +374,13 @@
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       renameAutocomplete.focused = true;
-      renameAutocomplete.noDebounce = true;
       renameAutocomplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        renameAutocomplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await renameAutocomplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       assert.isTrue(element.renameDialog!.disabled);
 
@@ -395,9 +410,13 @@
       // Setup focused manually - in headless mode Chrome sometimes doesn't
       // setup focus. waitEventLoop() doesn't help.
       renameAutocomplete.focused = true;
-      renameAutocomplete.noDebounce = true;
       renameAutocomplete.text = 'src/test.cpp';
+      // Focus happens after updateComplete, so we first wait for it explicitly.
+      await new Promise<void>(resolve => {
+        renameAutocomplete.addEventListener('focus', () => resolve());
+      });
       await element.updateComplete;
+      await renameAutocomplete.latestSuggestionUpdateComplete;
       assert.isTrue(queryStub.called);
       assert.isTrue(element.renameDialog!.disabled);
 
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index b096f76..e5991f6 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -500,11 +500,12 @@
   }
 
   private renderEditorView() {
-    // The `cache()` is required for re-using the editor view when switching
-    // back and forth between change, diff and editor views.
-    return cache(
-      this.isEditorView() ? html`<gr-editor-view></gr-editor-view>` : nothing
-    );
+    // For some reason caching the editor view caused an issue (b/269308770).
+    // We did not bother to root cause that issue, but instead let's forgo
+    // caching of the editor view. It does not help much anyway.
+    return this.isEditorView()
+      ? html`<gr-editor-view></gr-editor-view>`
+      : nothing;
   }
 
   private isEditorView() {
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.ts b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
index da9c0c9..d66fec0 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -14,11 +14,27 @@
 import {GrAnnotation} from '../embed/diff/gr-diff-highlight/gr-annotation';
 import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context';
 import {AppContext, injectAppContext} from '../services/app-context';
-import {Finalizable} from '../services/registry';
 import {PluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {
+  initVisibilityReporter,
+  initPerformanceReporter,
+  initErrorReporter,
+  initWebVitals,
+} from '../services/gr-reporting/gr-reporting_impl';
+import {Finalizable} from '../services/registry';
 
-export function initGlobalVariables(appContext: AppContext & Finalizable) {
+export function initGlobalVariables(
+  appContext: AppContext & Finalizable,
+  initializeReporting: boolean
+) {
   injectAppContext(appContext);
+  if (initializeReporting) {
+    const reportingService = appContext.reportingService;
+    initVisibilityReporter(reportingService);
+    initPerformanceReporter(reportingService);
+    initWebVitals(reportingService);
+    initErrorReporter(reportingService);
+  }
   window.GrAnnotation = GrAnnotation;
   window.GrPluginActionContext = GrPluginActionContext;
 }
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index 645f94a..ded0626 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -37,12 +37,6 @@
   createAppDependencies,
   Creator,
 } from '../services/app-context-init';
-import {
-  initVisibilityReporter,
-  initPerformanceReporter,
-  initErrorReporter,
-  initWebVitals,
-} from '../services/gr-reporting/gr-reporting_impl';
 import {html, LitElement} from 'lit';
 import {customElement} from 'lit/decorators.js';
 import {
@@ -50,14 +44,9 @@
   serviceWorkerInstallerToken,
 } from '../services/service-worker-installer';
 import {pluginLoaderToken} from './shared/gr-js-api-interface/gr-plugin-loader';
+import {getAppContext} from '../services/app-context';
 
-const appContext = createAppContext();
-initGlobalVariables(appContext);
-const reportingService = appContext.reportingService;
-initVisibilityReporter(reportingService);
-initPerformanceReporter(reportingService);
-initWebVitals(reportingService);
-initErrorReporter(reportingService);
+initGlobalVariables(createAppContext(), true);
 
 installPolymerResin(safeTypesBridge);
 
@@ -97,7 +86,7 @@
     };
 
     for (const [token, creator] of createAppDependencies(
-      appContext,
+      getAppContext(),
       resolver
     )) {
       injectDependency(token, creator);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index 6b4d670..cc2723e 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -401,8 +401,8 @@
     );
     input.text = 'newTest';
     input.input!.focus();
-    input.noDebounce = true;
     await element.updateComplete;
+    await input.latestSuggestionUpdateComplete;
     assert.isTrue(getSuggestionsStub.calledOnce);
     assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
     await waitUntil(() => makeSuggestionItemSpy.getCalls().length === 2);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index f7f8ea5..f8f7f9d 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -6,11 +6,14 @@
 import '@polymer/paper-input/paper-input';
 import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
 import '../gr-cursor-manager/gr-cursor-manager';
-import '../gr-icon/gr-icon';
 import '../../../styles/shared-styles';
 import {GrAutocompleteDropdown} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
 import {fire, fireEvent} from '../../../utils/event-util';
-import {debounce, DelayedTask} from '../../../utils/async-util';
+import {
+  debounce,
+  DelayedTask,
+  ResolvedDelayedTaskStatus,
+} from '../../../utils/async-util';
 import {PropertyType} from '../../../types/common';
 import {modifierPressed} from '../../../utils/dom-util';
 import {sharedStyles} from '../../../styles/shared-styles';
@@ -108,9 +111,6 @@
   @property({type: Boolean})
   disabled = false;
 
-  @property({type: Boolean, attribute: 'show-search-icon'})
-  showSearchIcon = false;
-
   /**
    * Vertical offset needed for an element with 20px line-height, 4px
    * padding and 1px border (30px height total). Plus 1px spacing between
@@ -154,12 +154,6 @@
   @property({type: Boolean, attribute: 'warn-uncommitted'})
   warnUncommitted = false;
 
-  /**
-   * When true, querying for suggestions is not debounced w/r/t keypresses
-   */
-  @property({type: Boolean, attribute: 'no-debounce'})
-  noDebounce = false;
-
   @property({type: Boolean, attribute: 'show-blue-focus-border'})
   showBlueFocusBorder = false;
 
@@ -187,6 +181,15 @@
 
   private updateSuggestionsTask?: DelayedTask;
 
+  /**
+   * @return Promise that resolves when suggestions are update.
+   */
+  get latestSuggestionUpdateComplete():
+    | Promise<ResolvedDelayedTaskStatus>
+    | undefined {
+    return this.updateSuggestionsTask?.promise;
+  }
+
   get nativeInput() {
     return (this.input!.inputElement as IronInputElement)
       .inputElement as HTMLInputElement;
@@ -195,15 +198,6 @@
   static override styles = [
     sharedStyles,
     css`
-      .searchIcon {
-        display: none;
-      }
-      .searchIcon.showSearchIcon {
-        display: inline-block;
-      }
-      gr-icon {
-        margin: 0 var(--spacing-xs);
-      }
       paper-input.borderless {
         border: none;
         padding: 0;
@@ -267,11 +261,7 @@
   }
 
   override willUpdate(changedProperties: PropertyValues) {
-    if (
-      changedProperties.has('text') ||
-      changedProperties.has('threshold') ||
-      changedProperties.has('noDebounce')
-    ) {
+    if (changedProperties.has('text') || changedProperties.has('threshold')) {
       this.updateSuggestions();
     }
     if (
@@ -307,12 +297,7 @@
         .label=${this.label}
       >
         <div slot="prefix">
-          <gr-icon
-            icon="search"
-            class="searchIcon ${this.computeShowSearchIconClass(
-              this.showSearchIcon
-            )}"
-          ></gr-icon>
+          <slot name="prefix"></slot>
         </div>
 
         <div slot="suffix">
@@ -422,12 +407,7 @@
   }
 
   updateSuggestions() {
-    if (
-      this.text === undefined ||
-      this.threshold === undefined ||
-      this.noDebounce === undefined
-    )
-      return;
+    if (this.text === undefined || this.threshold === undefined) return;
 
     // Reset suggestions for every update
     // This will also prevent from carrying over suggestions:
@@ -455,10 +435,11 @@
       return;
     }
 
+    const requestText = this.text;
     const update = () => {
       query(this.text)
         .then(suggestions => {
-          if (this.text !== this.text) {
+          if (requestText !== this.text) {
             // Late response.
             return;
           }
@@ -480,15 +461,11 @@
         });
     };
 
-    if (this.noDebounce) {
-      update();
-    } else {
-      this.updateSuggestionsTask = debounce(
-        this.updateSuggestionsTask,
-        update,
-        DEBOUNCE_WAIT_MS
-      );
-    }
+    this.updateSuggestionsTask = debounce(
+      this.updateSuggestionsTask,
+      update,
+      DEBOUNCE_WAIT_MS
+    );
   }
 
   setFocus(focused: boolean) {
@@ -682,10 +659,6 @@
       );
     }
   }
-
-  computeShowSearchIconClass(showSearchIcon: boolean) {
-    return showSearchIcon ? 'showSearchIcon' : '';
-  }
 }
 
 /**
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
index e593c0d..81949c7 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.ts
@@ -46,7 +46,7 @@
           tabindex="0"
         >
           <div slot="prefix">
-            <gr-icon icon="search" class="searchIcon"></gr-icon>
+            <slot name="prefix"> </slot>
           </div>
           <div slot="suffix">
             <slot name="suffix"> </slot>
@@ -96,7 +96,7 @@
           tabindex="0"
         >
           <div slot="prefix">
-            <gr-icon icon="search" class="searchIcon"></gr-icon>
+            <slot name="prefix"> </slot>
           </div>
           <div slot="suffix">
             <slot name="suffix"> </slot>
@@ -135,7 +135,7 @@
           tabindex="0"
         >
           <div slot="prefix">
-            <gr-icon icon="search" class="searchIcon"></gr-icon>
+            <slot name="prefix"> </slot>
           </div>
           <div slot="suffix">
             <slot name="suffix"> </slot>
@@ -391,7 +391,6 @@
 
     element.query = queryStub;
     await element.updateComplete;
-    element.noDebounce = false;
     focusOnInput();
     element.text = 'a';
 
@@ -413,7 +412,6 @@
   test('empty text results in no suggestions', async () => {
     element.text = '';
     element.threshold = 0;
-    element.noDebounce = false;
     await element.updateComplete;
     assert.equal(element.suggestions.length, 0);
   });
@@ -475,7 +473,6 @@
       assert.equal(element.suggestions.length, 1);
       element.text = '';
       element.threshold = 0;
-      element.noDebounce = false;
       await element.updateComplete;
       assert.equal(element.suggestions.length, 0);
     });
@@ -494,7 +491,6 @@
       await waitUntil(() => element.queryErrorMessage === 'Test error');
       element.text = '';
       element.threshold = 0;
-      element.noDebounce = false;
       await element.updateComplete;
       assert.isUndefined(element.queryErrorMessage);
     });
@@ -563,20 +559,6 @@
     assert.isTrue(element.focused);
   });
 
-  test('search icon shows with showSearchIcon property', async () => {
-    assert.equal(
-      getComputedStyle(queryAndAssert(element, 'gr-icon')).display,
-      'none'
-    );
-    element.showSearchIcon = true;
-    await element.updateComplete;
-
-    assert.notEqual(
-      getComputedStyle(queryAndAssert(element, 'gr-icon')).display,
-      'none'
-    );
-  });
-
   test('vertical offset overridden by param if it exists', async () => {
     assert.equal(suggestionsEl().verticalOffset, 31);
 
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 1ac89d6..c844d42 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -1053,6 +1053,11 @@
           replacement
         ),
         patchNum: this.comment.patch_set,
+        onCloseFixPreviewCallbacks: [
+          fixApplied => {
+            if (fixApplied) this.handleAppliedFix();
+          },
+        ],
       };
     }
     if (isRobot(this.comment) && this.comment.fix_suggestions.length > 0) {
@@ -1065,6 +1070,7 @@
           };
         }),
         patchNum: this.comment.patch_set,
+        onCloseFixPreviewCallbacks: [],
       };
     }
     throw new Error('unable to create preview fix event');
@@ -1132,6 +1138,18 @@
     fire(this, 'reply-to-comment', eventDetail);
   }
 
+  private handleAppliedFix() {
+    const message = this.comment?.message;
+    assert(!!message, 'empty message');
+    const eventDetail: ReplyToCommentEventDetail = {
+      content: 'Fix applied.',
+      userWantsToEdit: false,
+      unresolved: false,
+    };
+    // Handled by <gr-comment-thread>.
+    fire(this, 'reply-to-comment', eventDetail);
+  }
+
   private async handleShowFix() {
     // Handled top-level in the diff and change view components.
     fire(this, 'open-fix-preview', await this.createFixPreview());
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
index 25ee130..05b7fb5 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.ts
@@ -18,8 +18,10 @@
 } from '../../../utils/date-util';
 import {TimeFormat, DateFormat} from '../../../constants/constants';
 import {assertNever} from '../../../utils/common-util';
-import {Timestamp} from '../../../types/common';
-import {getAppContext} from '../../../services/app-context';
+import {PreferencesInfo, Timestamp} from '../../../types/common';
+import {resolve} from '../../../models/dependency';
+import {userModelToken} from '../../../models/user/user-model';
+import {subscribe} from '../../lit/subscription-controller';
 
 const TimeFormats = {
   TIME_12: 'h:mm A', // 2:14 PM
@@ -95,7 +97,7 @@
   @state()
   relative = false;
 
-  private readonly restApiService = getAppContext().restApiService;
+  private readonly getUserModel = resolve(this, userModelToken);
 
   static override get styles() {
     return [
@@ -108,17 +110,30 @@
     ];
   }
 
-  override render() {
-    if (!this.withTooltip) {
-      return this.renderDateString();
-    }
+  constructor() {
+    super();
+    subscribe(
+      this,
+      () => this.getUserModel().preferences$,
+      prefs => this.setPreferences(prefs)
+    );
+  }
 
-    const fullDateStr = this.computeFullDateStr();
-    if (!fullDateStr) {
-      return this.renderDateString();
-    }
+  // private but used by tests
+  setPreferences(prefs: PreferencesInfo) {
+    this.decideDateFormat(prefs.date_format);
+    this.decideTimeFormat(prefs.time_format);
+    this.relative =
+      this.forceRelative || Boolean(prefs?.relative_date_in_change_table);
+  }
+
+  override render() {
+    if (!this.withTooltip) return this.renderDateString();
+    const tooltip = this.computeFullDateStr();
+    if (!tooltip) return this.renderDateString();
+
     return html`
-      <gr-tooltip-content has-tooltip title=${fullDateStr}>
+      <gr-tooltip-content has-tooltip title=${tooltip}>
         ${this.renderDateString()}
       </gr-tooltip-content>
     `;
@@ -128,38 +143,11 @@
     return html` <span>${this.computeDateStr()}</span>`;
   }
 
-  override connectedCallback() {
-    super.connectedCallback();
-    this.loadPreferences();
-  }
-
   // private but used by tests
-  _getUtcOffsetString() {
+  getUtcOffsetString() {
     return utcOffsetString();
   }
 
-  // private but used by tests
-  async loadPreferences() {
-    const loggedIn = await this.restApiService.getLoggedIn();
-    if (!loggedIn) {
-      this.timeFormat = TimeFormats.TIME_24;
-      this.dateFormat = DateFormats.STD;
-      this.relative = this.forceRelative;
-      return;
-    }
-    await Promise.all([this.loadTimeFormat(), this.loadRelative()]);
-  }
-
-  // private but used in gr/file-list_test.ts
-  async loadTimeFormat() {
-    const preferences = await this.restApiService.getPreferences();
-    if (!preferences) {
-      throw Error('Preferences is not set');
-    }
-    this.decideTimeFormat(preferences.time_format);
-    this.decideDateFormat(preferences.date_format);
-  }
-
   private decideTimeFormat(timeFormat: TimeFormat) {
     switch (timeFormat) {
       case TimeFormat.HHMM_12:
@@ -195,12 +183,6 @@
     }
   }
 
-  private async loadRelative() {
-    const prefs = await this.restApiService.getPreferences();
-    this.relative =
-      this.forceRelative || Boolean(prefs?.relative_date_in_change_table);
-  }
-
   private computeDateStr() {
     if (!this.dateStr || !this.timeFormat || !this.dateFormat) {
       return '';
@@ -222,33 +204,24 @@
       if (isWithinHalfYear(now, date)) {
         format = this.dateFormat.short;
       }
-      if (this.showDateAndTime || this.showDateAndTime) {
+      if (this.showDateAndTime) {
         format = `${format} ${this.timeFormat}`;
       }
     }
     return formatDate(date, format);
   }
 
-  private computeFullDateStr() {
-    if (
-      [this.dateStr, this.timeFormat].includes(undefined) ||
-      !this.dateFormat
-    ) {
-      return undefined;
-    }
-
-    if (!this.dateStr) {
-      return '';
-    }
+  private computeFullDateStr(): string {
+    if (!this.dateStr) return '';
+    if (!this.timeFormat) return '';
+    if (!this.dateFormat) return '';
     const date = parseDate(this.dateStr as Timestamp);
-    if (!isValidDate(date)) {
-      return '';
-    }
-    let format = this.dateFormat.full + ', ';
-    format +=
+    if (!isValidDate(date)) return '';
+    const timeFormat =
       this.timeFormat === TimeFormats.TIME_12
         ? TimeFormats.TIME_12_WITH_SEC
         : TimeFormats.TIME_24_WITH_SEC;
-    return formatDate(date, format) + this._getUtcOffsetString();
+    const format = `dddd, ${this.dateFormat.full}, ${timeFormat}`;
+    return formatDate(date, format) + this.getUtcOffsetString();
   }
 }
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts
index d7c38df..98f2d82 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.ts
@@ -8,16 +8,15 @@
 import {GrDateFormatter} from './gr-date-formatter';
 import {parseDate} from '../../../utils/date-util';
 import {fixture, html, assert} from '@open-wc/testing';
-import {query, queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {query, queryAndAssert} from '../../../test/test-utils';
 import {GrTooltipContent} from '../gr-tooltip-content/gr-tooltip-content';
 import {Timestamp} from '../../../api/rest-api';
-import {PreferencesInfo} from '../../../types/common';
-import {createPreferences} from '../../../test/test-data-generators';
 import {
   createDefaultPreferences,
   DateFormat,
   TimeFormat,
 } from '../../../constants/constants';
+import {PreferencesInfo} from '../../../types/common';
 
 const basicTemplate = html`
   <gr-date-formatter withTooltip dateStr="2015-09-24 23:30:17.033000000">
@@ -41,6 +40,10 @@
     return d;
   }
 
+  function setPrefs(prefs: Partial<PreferencesInfo>) {
+    element.setPreferences({...createDefaultPreferences(), ...prefs});
+  }
+
   async function testDates(
     nowStr: string,
     dateStr: string,
@@ -68,23 +71,11 @@
     assert.equal(span.textContent?.trim(), expectedWithDateAndTime);
   }
 
-  function stubRestAPI(preferences?: PreferencesInfo) {
-    stubRestApi('getLoggedIn').resolves(preferences !== undefined);
-    stubRestApi('getPreferences').resolves(preferences);
-  }
-
   suite('STD + 24 hours time format preference', () => {
     setup(async () => {
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_24,
-        date_format: DateFormat.STD,
-        relative_date_in_change_table: false,
-      });
-
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.STD, time_format: TimeFormat.HHMM_24});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -93,7 +84,7 @@
         '2015-07-29 15:34:14.985000000',
         '15:34',
         '15:34',
-        'Jul 29, 2015, 15:34:14'
+        'Wednesday, Jul 29, 2015, 15:34:14'
       );
     });
 
@@ -103,7 +94,7 @@
         '2015-07-28 20:25:14.985000000',
         'Jul 28',
         'Jul 28 20:25',
-        'Jul 28, 2015, 20:25:14'
+        'Tuesday, Jul 28, 2015, 20:25:14'
       );
     });
 
@@ -113,7 +104,7 @@
         '2015-06-15 03:25:14.985000000',
         'Jun 15',
         'Jun 15 03:25',
-        'Jun 15, 2015, 03:25:14'
+        'Monday, Jun 15, 2015, 03:25:14'
       );
     });
 
@@ -123,22 +114,16 @@
         '2015-01-15 03:25:00.000000000',
         'Jan 15, 2015',
         'Jan 15, 2015 03:25',
-        'Jan 15, 2015, 03:25:00'
+        'Thursday, Jan 15, 2015, 03:25:00'
       );
     });
   });
 
   suite('US + 24 hours time format preference', () => {
     setup(async () => {
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_24,
-        date_format: DateFormat.US,
-        relative_date_in_change_table: false,
-      });
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.US, time_format: TimeFormat.HHMM_24});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -147,7 +132,7 @@
         '2015-07-29 15:34:14.985000000',
         '15:34',
         '15:34',
-        '07/29/15, 15:34:14'
+        'Wednesday, 07/29/15, 15:34:14'
       );
     });
 
@@ -157,7 +142,7 @@
         '2015-07-28 20:25:14.985000000',
         '07/28',
         '07/28 20:25',
-        '07/28/15, 20:25:14'
+        'Tuesday, 07/28/15, 20:25:14'
       );
     });
 
@@ -167,23 +152,16 @@
         '2015-06-15 03:25:14.985000000',
         '06/15',
         '06/15 03:25',
-        '06/15/15, 03:25:14'
+        'Monday, 06/15/15, 03:25:14'
       );
     });
   });
 
   suite('ISO + 24 hours time format preference', () => {
     setup(async () => {
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_24,
-        date_format: DateFormat.ISO,
-        relative_date_in_change_table: false,
-      });
-
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.ISO, time_format: TimeFormat.HHMM_24});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -192,7 +170,7 @@
         '2015-07-29 15:34:14.985000000',
         '15:34',
         '15:34',
-        '2015-07-29, 15:34:14'
+        'Wednesday, 2015-07-29, 15:34:14'
       );
     });
 
@@ -202,7 +180,7 @@
         '2015-07-28 20:25:14.985000000',
         '07-28',
         '07-28 20:25',
-        '2015-07-28, 20:25:14'
+        'Tuesday, 2015-07-28, 20:25:14'
       );
     });
 
@@ -212,23 +190,16 @@
         '2015-06-15 03:25:14.985000000',
         '06-15',
         '06-15 03:25',
-        '2015-06-15, 03:25:14'
+        'Monday, 2015-06-15, 03:25:14'
       );
     });
   });
 
   suite('EURO + 24 hours time format preference', () => {
     setup(async () => {
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_24,
-        date_format: DateFormat.EURO,
-        relative_date_in_change_table: false,
-      });
-
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.EURO, time_format: TimeFormat.HHMM_24});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -237,7 +208,7 @@
         '2015-07-29 15:34:14.985000000',
         '15:34',
         '15:34',
-        '29.07.2015, 15:34:14'
+        'Wednesday, 29.07.2015, 15:34:14'
       );
     });
 
@@ -247,7 +218,7 @@
         '2015-07-28 20:25:14.985000000',
         '28. Jul',
         '28. Jul 20:25',
-        '28.07.2015, 20:25:14'
+        'Tuesday, 28.07.2015, 20:25:14'
       );
     });
 
@@ -257,23 +228,16 @@
         '2015-06-15 03:25:14.985000000',
         '15. Jun',
         '15. Jun 03:25',
-        '15.06.2015, 03:25:14'
+        'Monday, 15.06.2015, 03:25:14'
       );
     });
   });
 
   suite('UK + 24 hours time format preference', () => {
     setup(async () => {
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_24,
-        date_format: DateFormat.UK,
-        relative_date_in_change_table: false,
-      });
-
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.UK, time_format: TimeFormat.HHMM_24});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -282,7 +246,7 @@
         '2015-07-29 15:34:14.985000000',
         '15:34',
         '15:34',
-        '29/07/2015, 15:34:14'
+        'Wednesday, 29/07/2015, 15:34:14'
       );
     });
 
@@ -292,7 +256,7 @@
         '2015-07-28 20:25:14.985000000',
         '28/07',
         '28/07 20:25',
-        '28/07/2015, 20:25:14'
+        'Tuesday, 28/07/2015, 20:25:14'
       );
     });
 
@@ -302,22 +266,16 @@
         '2015-06-15 03:25:14.985000000',
         '15/06',
         '15/06 03:25',
-        '15/06/2015, 03:25:14'
+        'Monday, 15/06/2015, 03:25:14'
       );
     });
   });
 
   suite('STD + 12 hours time format preference', () => {
     setup(async () => {
-      // relative_date_in_change_table is not set when false.
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_12,
-        date_format: DateFormat.STD,
-      });
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.STD, time_format: TimeFormat.HHMM_12});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -326,22 +284,16 @@
         '2015-07-29 15:34:14.985000000',
         '3:34 PM',
         '3:34 PM',
-        'Jul 29, 2015, 3:34:14 PM'
+        'Wednesday, Jul 29, 2015, 3:34:14 PM'
       );
     });
   });
 
   suite('US + 12 hours time format preference', () => {
     setup(async () => {
-      // relative_date_in_change_table is not set when false.
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_12,
-        date_format: DateFormat.US,
-      });
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.US, time_format: TimeFormat.HHMM_12});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -350,22 +302,16 @@
         '2015-07-29 15:34:14.985000000',
         '3:34 PM',
         '3:34 PM',
-        '07/29/15, 3:34:14 PM'
+        'Wednesday, 07/29/15, 3:34:14 PM'
       );
     });
   });
 
   suite('ISO + 12 hours time format preference', () => {
     setup(async () => {
-      // relative_date_in_change_table is not set when false.
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_12,
-        date_format: DateFormat.ISO,
-      });
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.ISO, time_format: TimeFormat.HHMM_12});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -374,22 +320,16 @@
         '2015-07-29 15:34:14.985000000',
         '3:34 PM',
         '3:34 PM',
-        '2015-07-29, 3:34:14 PM'
+        'Wednesday, 2015-07-29, 3:34:14 PM'
       );
     });
   });
 
   suite('EURO + 12 hours time format preference', () => {
     setup(async () => {
-      // relative_date_in_change_table is not set when false.
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_12,
-        date_format: DateFormat.EURO,
-      });
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.EURO, time_format: TimeFormat.HHMM_12});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -398,22 +338,16 @@
         '2015-07-29 15:34:14.985000000',
         '3:34 PM',
         '3:34 PM',
-        '29.07.2015, 3:34:14 PM'
+        'Wednesday, 29.07.2015, 3:34:14 PM'
       );
     });
   });
 
   suite('UK + 12 hours time format preference', () => {
     setup(async () => {
-      // relative_date_in_change_table is not set when false.
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_12,
-        date_format: DateFormat.UK,
-      });
       element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      setPrefs({date_format: DateFormat.UK, time_format: TimeFormat.HHMM_12});
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -422,22 +356,20 @@
         '2015-07-29 15:34:14.985000000',
         '3:34 PM',
         '3:34 PM',
-        '29/07/2015, 3:34:14 PM'
+        'Wednesday, 29/07/2015, 3:34:14 PM'
       );
     });
   });
 
   suite('relative date preference', () => {
     setup(async () => {
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_12,
+      element = await fixture(basicTemplate);
+      setPrefs({
         date_format: DateFormat.STD,
+        time_format: TimeFormat.HHMM_12,
         relative_date_in_change_table: true,
       });
-      element = await fixture(basicTemplate);
-      sinon.stub(element, '_getUtcOffsetString').returns('');
-      await element.loadPreferences();
+      sinon.stub(element, 'getUtcOffsetString').returns('');
     });
 
     test('Within 24 hours on same day', async () => {
@@ -446,7 +378,7 @@
         '2015-07-29 15:34:14.985000000',
         '5 hours ago',
         '5 hours ago',
-        'Jul 29, 2015, 3:34:14 PM'
+        'Wednesday, Jul 29, 2015, 3:34:14 PM'
       );
     });
 
@@ -456,21 +388,19 @@
         '2015-01-15 03:25:00.000000000',
         '8 months ago',
         '8 months ago',
-        'Jan 15, 2015, 3:25:00 AM'
+        'Thursday, Jan 15, 2015, 3:25:00 AM'
       );
     });
   });
 
   suite('logged in', () => {
     setup(async () => {
-      stubRestAPI({
-        ...createPreferences(),
-        time_format: TimeFormat.HHMM_12,
+      element = await fixture(basicTemplate);
+      setPrefs({
         date_format: DateFormat.US,
+        time_format: TimeFormat.HHMM_12,
         relative_date_in_change_table: true,
       });
-      element = await fixture(basicTemplate);
-      await element.loadPreferences();
     });
 
     test('Preferences are respected', () => {
@@ -483,13 +413,12 @@
 
   suite('logged out', () => {
     setup(async () => {
-      stubRestAPI(undefined);
       element = await fixture(basicTemplate);
-      await element.loadPreferences();
+      setPrefs({});
     });
 
     test('Default preferences are respected', () => {
-      assert.equal(element.timeFormat, 'HH:mm');
+      assert.equal(element.timeFormat, 'h:mm A');
       assert.equal(element.dateFormat?.short, 'MMM DD');
       assert.equal(element.dateFormat?.full, 'MMM DD, YYYY');
       assert.isFalse(element.relative);
@@ -498,9 +427,8 @@
 
   suite('with tooltip', () => {
     setup(async () => {
-      stubRestAPI(createDefaultPreferences());
       element = await fixture(basicTemplate);
-      await element.loadPreferences();
+      setPrefs({});
       await element.updateComplete;
     });
 
@@ -515,9 +443,8 @@
 
   suite('without tooltip', () => {
     setup(async () => {
-      stubRestAPI(createDefaultPreferences());
       element = await fixture(lightTemplate);
-      await element.loadPreferences();
+      setPrefs({});
       await element.updateComplete;
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index cf279ba..fcebeea 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -76,12 +76,12 @@
         element,
         /* HTML */ `
           <pre class="plaintext">
-            http://google.com/<a
+            <a
               href="http://google.com/LinkRewriteMe"
               rel="noopener"
               target="_blank"
             >
-              LinkRewriteMe
+            http://google.com/LinkRewriteMe
             </a>
           </pre>
         `
@@ -159,8 +159,15 @@
         element,
         /* HTML */ `
           <pre class="plaintext">
-          text with plain link: http://google.com
-        text with config link:
+          text with plain link:
+          <a
+            href="http://google.com"
+            rel="noopener"
+            target="_blank"
+          >
+            http://google.com
+          </a>
+          text with config link:
             <a
               href="http://google.com/LinkRewriteMe"
               rel="noopener"
@@ -269,7 +276,14 @@
         /* HTML */ `
           <pre class="plaintext">
           text
-        text with plain link: http://google.com
+        text with plain link:
+        <a
+          href="http://google.com"
+          rel="noopener"
+          target="_blank"
+        >
+          http://google.com
+        </a>
         text with config link:
           <a
             href="http://google.com/LinkRewriteMe"
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
index 13a6b00..b97ae59 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row.ts
@@ -160,6 +160,8 @@
         ${this.renderLineNumberCell(Side.RIGHT)}
         ${this.renderSignCell(Side.RIGHT)} ${this.renderContentCell(Side.RIGHT)}
       </tr>
+      ${this.renderPostLineSlot(Side.LEFT)}
+      ${this.renderPostLineSlot(Side.RIGHT)}
     `;
     if (this.addTableWrapperForTesting) {
       return html`<table>
@@ -456,6 +458,13 @@
         id=${this.contentId(side)}
       >${textElement}</div>`;
   }
+
+  private renderPostLineSlot(side: Side) {
+    const lineNumber = this.lineNumber(side);
+    return lineNumber && Number.isInteger(lineNumber)
+      ? html`<slot name="post-${side}-line-${lineNumber}"></slot>`
+      : nothing;
+  }
 }
 
 declare global {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
index 1c7b311..42d30aa 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-row_test.ts
@@ -87,6 +87,8 @@
                 </div>
               </td>
             </tr>
+            <slot name="post-left-line-1"></slot>
+            <slot name="post-right-line-1"></slot>
           </tbody>
         </table>
       `
@@ -147,6 +149,8 @@
                 </div>
               </td>
             </tr>
+            <slot name="post-left-line-1"></slot>
+            <slot name="post-right-line-1"></slot>
           </tbody>
         </table>
       `
@@ -201,6 +205,7 @@
                   <slot name="right-1"> </slot>
                 </div>
               </td>
+              <slot name="post-right-line-1"></slot>
             </tr>
           </tbody>
         </table>
@@ -257,6 +262,7 @@
                 <div class="contentText gr-diff" data-side="right"></div>
               </td>
             </tr>
+            <slot name="post-left-line-1"></slot>
           </tbody>
         </table>
       `
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
index b952a3d..d40fdda 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section.ts
@@ -26,6 +26,7 @@
 import '../gr-context-controls/gr-context-controls';
 import '../gr-range-header/gr-range-header';
 import './gr-diff-row';
+import {when} from 'lit/directives/when.js';
 
 @customElement('gr-diff-section')
 export class GrDiffSection extends LitElement {
@@ -170,7 +171,7 @@
     `;
     const moveCell = html`
       <td class=${diffClasses('moveHeader')}>
-        <gr-range-header class=${diffClasses()} icon="gr-icons:move-item">
+        <gr-range-header class=${diffClasses()} icon="move_item">
           ${this.renderMoveDescription(movedIn)}
         </gr-range-header>
       </td>
@@ -179,8 +180,13 @@
       <tr
         class=${diffClasses('moveControls', movedIn ? 'movedIn' : 'movedOut')}
       >
-        ${lineNumberCell} ${signCell} ${movedIn ? plainCell : moveCell}
-        ${lineNumberCell} ${signCell} ${movedIn ? moveCell : plainCell}
+        ${when(
+          this.isUnifiedDiff(),
+          () => html`${lineNumberCell} ${lineNumberCell} ${moveCell}`,
+          () => html`${lineNumberCell} ${signCell}
+          ${movedIn ? plainCell : moveCell} ${lineNumberCell} ${signCell}
+          ${movedIn ? moveCell : plainCell}`
+        )}
       </tr>
     `;
   }
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
index 33b3df0..381f9b2 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-builder/gr-diff-section_test.ts
@@ -9,7 +9,8 @@
 import {fixture, html, assert} from '@open-wc/testing';
 import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
 import {GrDiffLine} from '../gr-diff/gr-diff-line';
-import {GrDiffLineType} from '../../../api/diff';
+import {DiffViewMode, GrDiffLineType} from '../../../api/diff';
+import {waitQueryAndAssert} from '../../../test/test-utils';
 
 suite('gr-diff-section test', () => {
   let element: GrDiffSection;
@@ -22,6 +23,93 @@
     await element.updateComplete;
   });
 
+  suite('move controls', async () => {
+    setup(async () => {
+      const lines = [new GrDiffLine(GrDiffLineType.BOTH, 1, 1)];
+      lines[0].text = 'asdf';
+      const group = new GrDiffGroup({
+        type: GrDiffGroupType.BOTH,
+        lines,
+        moveDetails: {changed: false, range: {start: 1, end: 2}},
+      });
+      element.group = group;
+      await element.updateComplete;
+    });
+
+    test('side-by-side', async () => {
+      const row = await waitQueryAndAssert(element, 'tr.moveControls');
+      // Semantic dom diff has a problem with just comparing table rows or
+      // cells directly. So as a workaround put the row into an empty test
+      // table.
+      const testTable = document.createElement('table');
+      testTable.appendChild(row);
+      assert.dom.equal(
+        testTable,
+        /* HTML */ `
+          <table>
+            <tbody>
+              <tr class="gr-diff moveControls movedOut">
+                <td class="gr-diff moveControlsLineNumCol"></td>
+                <td class="gr-diff sign"></td>
+                <td class="gr-diff moveHeader">
+                  <gr-range-header class="gr-diff" icon="move_item">
+                    <div class="gr-diff">
+                      <span class="gr-diff"> Moved to lines </span>
+                      <a class="gr-diff" href="#1"> 1 </a>
+                      <span class="gr-diff"> - </span>
+                      <a class="gr-diff" href="#2"> 2 </a>
+                    </div>
+                  </gr-range-header>
+                </td>
+                <td class="gr-diff moveControlsLineNumCol"></td>
+                <td class="gr-diff sign"></td>
+                <td class="gr-diff"></td>
+              </tr>
+            </tbody>
+          </table>
+        `,
+        {}
+      );
+    });
+
+    test('unified', async () => {
+      element.renderPrefs = {
+        ...element.renderPrefs,
+        view_mode: DiffViewMode.UNIFIED,
+      };
+      const row = await waitQueryAndAssert(element, 'tr.moveControls');
+      // Semantic dom diff has a problem with just comparing table rows or
+      // cells directly. So as a workaround put the row into an empty test
+      // table.
+      const testTable = document.createElement('table');
+      testTable.appendChild(row);
+      assert.dom.equal(
+        testTable,
+        /* HTML */ `
+          <table>
+            <tbody>
+              <tr class="gr-diff moveControls movedOut">
+                <td class="gr-diff moveControlsLineNumCol"></td>
+                <td class="gr-diff moveControlsLineNumCol"></td>
+                <td class="gr-diff moveHeader">
+                  <gr-range-header class="gr-diff" icon="move_item">
+                    <div class="gr-diff">
+                      <span class="gr-diff"> Moved to lines </span>
+                      <a class="gr-diff" href="#1"> 1 </a>
+                      <span class="gr-diff"> - </span>
+                      <a class="gr-diff" href="#2"> 2 </a>
+                    </div>
+                  </gr-range-header>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        `,
+        {}
+      );
+    });
+  });
+
   test('3 normal unchanged rows', async () => {
     const lines = [
       new GrDiffLine(GrDiffLineType.BOTH, 1, 1),
@@ -38,8 +126,14 @@
       element,
       /* HTML */ `
         <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+        <slot name="post-left-line-1"></slot>
+        <slot name="post-right-line-1"></slot>
         <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+        <slot name="post-left-line-1"></slot>
+        <slot name="post-right-line-1"></slot>
         <gr-diff-row class="left-1 right-1"> </gr-diff-row>
+        <slot name="post-left-line-1"></slot>
+        <slot name="post-right-line-1"></slot>
         <table>
           <tbody class="both gr-diff section">
             <tr
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts b/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
new file mode 100644
index 0000000..8fbda14
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {Observable} from 'rxjs';
+import {filter} from 'rxjs/operators';
+import {
+  DiffInfo,
+  DiffPreferencesInfo,
+  RenderPreferences,
+} from '../../../api/diff';
+import {define} from '../../../models/dependency';
+import {Model} from '../../../models/model';
+import {isDefined} from '../../../types/types';
+import {select} from '../../../utils/observable-util';
+
+export interface DiffState {
+  diff: DiffInfo;
+  path?: string;
+  renderPrefs: RenderPreferences;
+  diffPrefs: DiffPreferencesInfo;
+}
+
+export const diffModelToken = define<DiffModel>('diff-model');
+
+export class DiffModel extends Model<DiffState | undefined> {
+  readonly diff$: Observable<DiffInfo> = select(
+    this.state$.pipe(filter(isDefined)),
+    diffState => diffState.diff
+  );
+
+  readonly path$: Observable<string | undefined> = select(
+    this.state$.pipe(filter(isDefined)),
+    diffState => diffState.path
+  );
+
+  readonly renderPrefs$: Observable<RenderPreferences> = select(
+    this.state$.pipe(filter(isDefined)),
+    diffState => diffState.renderPrefs
+  );
+
+  readonly diffPrefs$: Observable<DiffPreferencesInfo> = select(
+    this.state$.pipe(filter(isDefined)),
+    diffState => diffState.diffPrefs
+  );
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index a0526ed..b9a01ce 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -69,6 +69,8 @@
 import {classMap} from 'lit/directives/class-map.js';
 import {iconStyles} from '../../../styles/gr-icon-styles';
 import {expandFileMode} from '../../../utils/file-util';
+import {DiffModel, diffModelToken} from '../gr-diff-model/gr-diff-model';
+import {provide} from '../../../models/dependency';
 
 const NO_NEWLINE_LEFT = 'No newline at end of left file.';
 const NO_NEWLINE_RIGHT = 'No newline at end of right file.';
@@ -138,10 +140,7 @@
   prefs?: DiffPreferencesInfo;
 
   @property({type: Object})
-  renderPrefs?: RenderPreferences;
-
-  @property({type: Boolean})
-  displayLine = false;
+  renderPrefs: RenderPreferences = {};
 
   @property({type: Boolean})
   isImageDiff?: boolean;
@@ -268,6 +267,8 @@
   // Private but used in tests.
   diffBuilder = new GrDiffBuilderElement();
 
+  private diffModel = new DiffModel(undefined);
+
   static override get styles() {
     return [
       iconStyles,
@@ -987,6 +988,7 @@
 
   constructor() {
     super();
+    provide(this, diffModelToken, () => this.diffModel);
     this.addEventListener('create-range-comment', (e: Event) =>
       this.handleCreateRangeComment(e as CustomEvent)
     );
@@ -1081,7 +1083,6 @@
       unified: this.viewMode === DiffViewMode.UNIFIED,
       sideBySide: this.viewMode === DiffViewMode.SIDE_BY_SIDE,
       canComment: this.loggedIn,
-      displayLine: this.displayLine,
     };
     return html`
       <div class=${classMap(cssClasses)} @click=${this.handleTap}>
@@ -1444,6 +1445,7 @@
 
   private prefsChanged() {
     if (!this.prefs) return;
+    this.diffModel.updateState({diffPrefs: this.prefs});
 
     this.blame = null;
     this.updatePreferenceStyles();
@@ -1514,7 +1516,7 @@
   }
 
   private renderPrefsChanged() {
-    if (!this.renderPrefs) return;
+    this.diffModel.updateState({renderPrefs: this.renderPrefs});
     if (this.renderPrefs.hide_left_side) {
       this.classList.add('no-left');
     }
@@ -1581,7 +1583,7 @@
   // Private but used in tests.
   async renderDiffTable() {
     this.unobserveNodes();
-    if (!this.prefs) {
+    if (!this.diff || !this.prefs) {
       fireEvent(this, 'render');
       return;
     }
@@ -1600,8 +1602,15 @@
 
     const keyLocations = this.computeKeyLocations();
 
+    this.diffModel.setState({
+      diff: this.diff,
+      path: this.path,
+      renderPrefs: this.renderPrefs,
+      diffPrefs: this.prefs,
+    });
+
     // TODO: Setting tons of public properties like this is obviously a code
-    // smell. We are planning to introduce a diff model for managing all this
+    // smell. We are introducing a diff model for managing all this
     // data. Then diff builder will only need access to that model.
     this.diffBuilder.prefs = this.getBypassPrefs();
     this.diffBuilder.renderPrefs = this.renderPrefs;
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index ce1393d..4adb1cf 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -3141,18 +3141,6 @@
       assert.isFalse(element.classList.contains('no-left'));
     });
 
-    test('view does not start with displayLine classList', () => {
-      const container = queryAndAssert(element, '.diffContainer');
-      assert.isFalse(container.classList.contains('displayLine'));
-    });
-
-    test('displayLine class added when displayLine is true', async () => {
-      element.displayLine = true;
-      await element.updateComplete;
-      const container = queryAndAssert(element, '.diffContainer');
-      assert.isTrue(container.classList.contains('displayLine'));
-    });
-
     suite('binary diffs', () => {
       test('render binary diff', async () => {
         element.prefs = {
@@ -3968,6 +3956,7 @@
 
     setup(async () => {
       element.prefs = {...MINIMAL_PREFS};
+      element.diff = createDiff();
       renderStub = sinon.stub(element.diffBuilder, 'render');
       await element.updateComplete;
     });
diff --git a/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header_test.ts b/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header_test.ts
new file mode 100644
index 0000000..b31197d16
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-range-header/gr-range-header_test.ts
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-range-header';
+import {GrRangeHeader} from './gr-range-header';
+import {fixture, html, assert} from '@open-wc/testing';
+
+suite('gr-range-header test', () => {
+  let element: GrRangeHeader;
+
+  setup(async () => {
+    element = await fixture<GrRangeHeader>(
+      html`<gr-range-header></gr-range-header>`
+    );
+    await element.updateComplete;
+  });
+
+  test('renders', async () => {
+    element.filled = true;
+    element.icon = 'test-icon';
+    await element.updateComplete;
+    assert.shadowDom.equal(
+      element,
+      /* HTML */ `
+        <div class="row">
+          <gr-icon
+            aria-hidden="true"
+            class="icon"
+            filled
+            icon="test-icon"
+          ></gr-icon>
+          <slot></slot>
+        </div>
+      `
+    );
+  });
+});
diff --git a/polygerrit-ui/app/models/accounts-model/accounts-model.ts b/polygerrit-ui/app/models/accounts-model/accounts-model.ts
index 2bf6068..1c67857 100644
--- a/polygerrit-ui/app/models/accounts-model/accounts-model.ts
+++ b/polygerrit-ui/app/models/accounts-model/accounts-model.ts
@@ -8,11 +8,14 @@
 import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {UserId} from '../../types/common';
 import {getUserId, isDetailedAccount} from '../../utils/account-util';
+import {hasOwnProperty} from '../../utils/common-util';
 import {define} from '../dependency';
 import {Model} from '../model';
 
 export interface AccountsState {
-  accounts: {[id: UserId]: AccountDetailInfo};
+  accounts: {
+    [id: UserId]: AccountDetailInfo | AccountInfo;
+  };
 }
 
 export const accountsModelToken = define<AccountsModel>('accounts-model');
@@ -24,33 +27,36 @@
     });
   }
 
-  private updateStateAccount(id: UserId, account?: AccountDetailInfo) {
+  private updateStateAccount(
+    id: UserId,
+    account: AccountDetailInfo | AccountInfo
+  ) {
     if (!account) return;
     const current = {...this.getState()};
     current.accounts = {...current.accounts, [id]: account};
     this.setState(current);
   }
 
-  async getAccount(partialAccount: AccountInfo) {
+  async getAccount(
+    partialAccount: AccountInfo
+  ): Promise<AccountDetailInfo | AccountInfo> {
     const current = this.getState();
     const id = getUserId(partialAccount);
-    if (current.accounts[id]) return current.accounts[id];
+    if (hasOwnProperty(current.accounts, id)) return current.accounts[id];
     // It is possible to add emails to CC when they don't have a Gerrit
-    // account. In this case getAccountDetails will return a 404 error hence
-    // pass an empty error function to handle that.
+    // account. In this case getAccountDetails will return a 404 error then
+    // we at least use what is in partialAccount.
     const account = await this.restApiService.getAccountDetails(id, () => {
-      this.updateStateAccount(id, partialAccount as AccountDetailInfo);
+      this.updateStateAccount(id, partialAccount);
       return;
     });
     if (account) this.updateStateAccount(id, account);
-    return account;
+    return account ?? partialAccount;
   }
 
   async fillDetails(account: AccountInfo) {
     if (!isDetailedAccount(account)) {
-      if (account.email) return await this.getAccount({email: account.email});
-      else if (account._account_id)
-        return await this.getAccount({_account_id: account._account_id});
+      return await this.getAccount(account);
     }
     return account;
   }
diff --git a/polygerrit-ui/app/models/checks/checks-util.ts b/polygerrit-ui/app/models/checks/checks-util.ts
index ba43eb4..da05bbe 100644
--- a/polygerrit-ui/app/models/checks/checks-util.ts
+++ b/polygerrit-ui/app/models/checks/checks-util.ts
@@ -121,6 +121,7 @@
   const eventDetail: OpenFixPreviewEventDetail = {
     patchNum: result.patchset as PatchSetNumber,
     fixSuggestions,
+    onCloseFixPreviewCallbacks: [],
   };
   return {
     name: 'Show Fix',
diff --git a/polygerrit-ui/app/models/views/admin_test.ts b/polygerrit-ui/app/models/views/admin_test.ts
index b6089af..0881018 100644
--- a/polygerrit-ui/app/models/views/admin_test.ts
+++ b/polygerrit-ui/app/models/views/admin_test.ts
@@ -3,28 +3,34 @@
  * Copyright 2022 Google LLC
  * SPDX-License-Identifier: Apache-2.0
  */
-import {assert} from '@open-wc/testing';
-import {PageContext} from '../../elements/core/gr-router/gr-page';
 import {GerritView} from '../../services/router/router-model';
 import '../../test/common-test-setup';
-import {AdminChildView, PLUGIN_LIST_ROUTE} from './admin';
+import {assertRouteFalse, assertRouteState} from '../../test/test-utils';
+import {
+  AdminChildView,
+  AdminViewState,
+  createAdminUrl,
+  PLUGIN_LIST_ROUTE,
+} from './admin';
 
 suite('admin view model', () => {
   suite('routes', () => {
     test('PLUGIN_LIST', () => {
-      const {urlPattern: pattern, createState} = PLUGIN_LIST_ROUTE;
+      assertRouteFalse(PLUGIN_LIST_ROUTE, 'admin/plugins');
+      assertRouteFalse(PLUGIN_LIST_ROUTE, '//admin/plugins');
+      assertRouteFalse(PLUGIN_LIST_ROUTE, '//admin/plugins?');
+      assertRouteFalse(PLUGIN_LIST_ROUTE, '/admin/plugins//');
 
-      assert.isTrue(pattern.test('/admin/plugins'));
-      assert.isTrue(pattern.test('/admin/plugins/'));
-      assert.isFalse(pattern.test('admin/plugins'));
-      assert.isFalse(pattern.test('//admin/plugins'));
-      assert.isFalse(pattern.test('//admin/plugins?'));
-      assert.isFalse(pattern.test('/admin/plugins//'));
-
-      assert.deepEqual(createState(new PageContext('')), {
+      const state: AdminViewState = {
         view: GerritView.ADMIN,
         adminView: AdminChildView.PLUGINS,
-      });
+      };
+      assertRouteState<AdminViewState>(
+        PLUGIN_LIST_ROUTE,
+        '/admin/plugins',
+        state,
+        createAdminUrl
+      );
     });
   });
 });
diff --git a/polygerrit-ui/app/models/views/dashboard.ts b/polygerrit-ui/app/models/views/dashboard.ts
index 74523db..d2e7995 100644
--- a/polygerrit-ui/app/models/views/dashboard.ts
+++ b/polygerrit-ui/app/models/views/dashboard.ts
@@ -10,7 +10,21 @@
 import {encodeURL, getBaseUrl} from '../../utils/url-util';
 import {define} from '../dependency';
 import {Model} from '../model';
-import {ViewState} from './base';
+import {Route, ViewState} from './base';
+
+export const PROJECT_DASHBOARD_ROUTE: Route<DashboardViewState> = {
+  urlPattern: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
+  createState: ctx => {
+    const project = (ctx.params[0] ?? '') as RepoName;
+    const dashboard = (ctx.params[1] ?? '') as DashboardId;
+    const state: DashboardViewState = {
+      view: GerritView.DASHBOARD,
+      project,
+      dashboard,
+    };
+    return state;
+  },
+};
 
 export interface DashboardViewState extends ViewState {
   view: GerritView.DASHBOARD;
diff --git a/polygerrit-ui/app/models/views/dashboard_test.ts b/polygerrit-ui/app/models/views/dashboard_test.ts
index a7620dd..9509977 100644
--- a/polygerrit-ui/app/models/views/dashboard_test.ts
+++ b/polygerrit-ui/app/models/views/dashboard_test.ts
@@ -5,11 +5,36 @@
  */
 import {assert} from '@open-wc/testing';
 import {RepoName} from '../../api/rest-api';
+import {GerritView} from '../../services/router/router-model';
 import '../../test/common-test-setup';
+import {assertRouteFalse, assertRouteState} from '../../test/test-utils';
 import {DashboardId} from '../../types/common';
-import {createDashboardUrl} from './dashboard';
+import {
+  createDashboardUrl,
+  DashboardViewState,
+  PROJECT_DASHBOARD_ROUTE,
+} from './dashboard';
 
 suite('dashboard view state tests', () => {
+  suite('routes', () => {
+    test('PROJECT_DASHBOARD_ROUTE', () => {
+      assertRouteFalse(PROJECT_DASHBOARD_ROUTE, '/p//+/dashboard/qwer');
+      assertRouteFalse(PROJECT_DASHBOARD_ROUTE, '/p/asdf/+/dashboard/');
+
+      const state: DashboardViewState = {
+        view: GerritView.DASHBOARD,
+        project: 'asdf' as RepoName,
+        dashboard: 'qwer' as DashboardId,
+      };
+      assertRouteState(
+        PROJECT_DASHBOARD_ROUTE,
+        '/p/asdf/+/dashboard/qwer',
+        state,
+        createDashboardUrl
+      );
+    });
+  });
+
   suite('createDashboardUrl()', () => {
     test('self dashboard', () => {
       assert.equal(createDashboardUrl({}), '/dashboard/self');
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 0d0c88f..610d8f3 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -769,7 +769,7 @@
     userId: AccountId | EmailAddress,
     errFn?: ErrorCallback
   ): Promise<AccountDetailInfo | undefined> {
-    return this._restApiHelper.fetchJSON({
+    return this._fetchSharedCacheURL({
       url: `/accounts/${encodeURIComponent(userId)}/detail`,
       anonymizedUrl: '/accounts/*/detail',
       errFn,
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index aed58d8..365bb16 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -6,7 +6,7 @@
 // TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
 // https://github.com/Polymer/polymer-resin/issues/9 is resolved.
 import '../scripts/bundled-polymer';
-import {AppContext} from '../services/app-context';
+import {getAppContext} from '../services/app-context';
 import {Finalizable} from '../services/registry';
 import {
   createTestAppContext,
@@ -60,7 +60,6 @@
 });
 
 let testSetupTimestampMs = 0;
-let appContext: AppContext & Finalizable;
 
 const injectedDependencies: Map<
   DependencyToken<unknown>,
@@ -101,11 +100,10 @@
   // If the following asserts fails - then window.stub is
   // overwritten by some other code.
   assert.equal(getCleanupsCount(), 0);
-  appContext = createTestAppContext();
-  initGlobalVariables(appContext);
+  initGlobalVariables(createTestAppContext(), false);
 
-  finalizers.push(appContext);
-  const dependencies = createTestDependencies(appContext, testResolver);
+  finalizers.push(getAppContext());
+  const dependencies = createTestDependencies(getAppContext(), testResolver);
   for (const [token, provider] of dependencies) {
     injectDependency(token, provider);
   }
@@ -124,7 +122,7 @@
   // `awaitPluginsLoaded` will rely on that to kick off,
   // in testing, we want to kick start this earlier.
   testResolver(pluginLoaderToken).loadPlugins([]);
-  testOnlyResetGrRestApiSharedObjects(appContext.authService);
+  testOnlyResetGrRestApiSharedObjects(getAppContext().authService);
 });
 
 export function removeRequestDependencyListener() {
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index a5bc4bf..94e7f79 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -362,6 +362,7 @@
     return Promise.resolve({});
   },
   getPreferences(): Promise<PreferencesInfo | undefined> {
+    // TODO: Use createDefaultPreferences() instead.
     return Promise.resolve(createPreferences());
   },
   getProjectConfig(): Promise<ConfigInfo | undefined> {
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 05e4df7..5abac09 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -73,21 +73,17 @@
 import {
   AccountsVisibility,
   AccountTag,
-  AppTheme,
   AuthType,
   ChangeStatus,
   CommentSide,
-  DateFormat,
-  DefaultBase,
+  createDefaultPreferences,
   DefaultDisplayNameConfig,
-  DiffViewMode,
   EmailStrategy,
   InheritedBooleanInfoConfiguredValue,
   MergeabilityComputationBehavior,
   RequirementStatus,
   RevisionKind,
   SubmitType,
-  TimeFormat,
 } from '../constants/constants';
 import {formatDate} from '../utils/date-util';
 import {GetDiffCommentsOutput} from '../services/gr-rest-api/gr-rest-api';
@@ -687,18 +683,12 @@
   };
 }
 
-// TODO: Maybe reconcile with createDefaultPreferences() in constants.ts.
+// TODO: Do not change the values of createDefaultPreferences() here.
 export function createPreferences(): PreferencesInfo {
   return {
+    ...createDefaultPreferences(),
     changes_per_page: 10,
-    theme: AppTheme.AUTO,
-    date_format: DateFormat.ISO,
-    time_format: TimeFormat.HHMM_24,
-    diff_view: DiffViewMode.SIDE_BY_SIDE,
-    my: [],
-    change_table: [],
     email_strategy: EmailStrategy.ENABLED,
-    default_base_for_merges: DefaultBase.AUTO_MERGE,
     allow_browser_notifications: true,
   };
 }
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index c400d9c..19c3a7b 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -14,6 +14,8 @@
 import {Observable} from 'rxjs';
 import {filter, take, timeout} from 'rxjs/operators';
 import {assert} from '@open-wc/testing';
+import {Route, ViewState} from '../models/views/base';
+import {PageContext} from '../elements/core/gr-router/gr-page';
 export {query, queryAll, queryAndAssert} from '../utils/common-util';
 
 export interface MockPromise<T> extends Promise<T> {
@@ -328,3 +330,26 @@
   };
   return new Proxy(obj, handler) as unknown as T;
 }
+
+export function assertRouteState<T extends ViewState>(
+  route: Route<T>,
+  path: string,
+  state: T,
+  createUrl: (state: T) => string
+) {
+  const {urlPattern, createState} = route;
+  const ctx = new PageContext(path);
+  const matches = ctx.match(urlPattern);
+  assert.isTrue(matches);
+  assert.deepEqual(createState(ctx), state);
+  assert.equal(path, createUrl(state));
+}
+
+export function assertRouteFalse<T extends ViewState>(
+  route: Route<T>,
+  path: string
+) {
+  const ctx = new PageContext(path);
+  const matches = ctx.match(route.urlPattern);
+  assert.isFalse(matches);
+}
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index 2cc6742..e2612b0 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -26,7 +26,6 @@
   MOVED_LINK_CLICKED = 'moved-link-clicked',
   NETWORK_ERROR = 'network-error',
   OPEN_FIX_PREVIEW = 'open-fix-preview',
-  CLOSE_FIX_PREVIEW = 'close-fix-preview',
   PAGE_ERROR = 'page-error',
   RELOAD = 'reload',
   REPLY = 'reply',
@@ -63,7 +62,6 @@
     'line-cursor-moved-out': LineNumberEvent;
     'moved-link-clicked': MovedLinkClickedEvent;
     'open-fix-preview': OpenFixPreviewEvent;
-    'close-fix-preview': CloseFixPreviewEvent;
     'reply-to-comment': ReplyToCommentEvent;
     /* prettier-ignore */
     'reload': ReloadEvent;
@@ -156,13 +154,10 @@
 export interface OpenFixPreviewEventDetail {
   patchNum: PatchSetNum;
   fixSuggestions: FixSuggestionInfo[];
+  onCloseFixPreviewCallbacks: ((fixapplied: boolean) => void)[];
 }
 export type OpenFixPreviewEvent = CustomEvent<OpenFixPreviewEventDetail>;
 
-export interface CloseFixPreviewEventDetail {
-  fixApplied: boolean;
-}
-export type CloseFixPreviewEvent = CustomEvent<CloseFixPreviewEventDetail>;
 export interface ReplyToCommentEventDetail {
   content: string;
   userWantsToEdit: boolean;
diff --git a/polygerrit-ui/app/utils/async-util.ts b/polygerrit-ui/app/utils/async-util.ts
index cae6319..752de62 100644
--- a/polygerrit-ui/app/utils/async-util.ts
+++ b/polygerrit-ui/app/utils/async-util.ts
@@ -37,6 +37,11 @@
 
 export const _testOnly_allTasks = new Map<number, DelayedTask>();
 
+export enum ResolvedDelayedTaskStatus {
+  CALLBACK_EXECUTED = 'CALLBACK_EXECUTED',
+  TASK_CANCELLED = 'TASK_CANCELLED',
+}
+
 /**
  * This is just a very simple and small wrapper around setTimeout(). Instead of
  * the usual:
@@ -52,34 +57,54 @@
  * It is just nicer to have an object for this instead of a number as a handle.
  */
 export class DelayedTask {
-  private timer?: number;
+  private timerId?: number;
+
+  /**
+   * Promise that is resolved after the callback is run or the task is
+   * cancelled.
+   */
+  public readonly promise: Promise<ResolvedDelayedTaskStatus>;
+
+  private resolvePromise?: (
+    value: ResolvedDelayedTaskStatus | PromiseLike<ResolvedDelayedTaskStatus>
+  ) => void;
 
   constructor(private callback: () => void, waitMs = 0) {
-    this.timer = window.setTimeout(() => {
-      if (this.timer) _testOnly_allTasks.delete(this.timer);
-      this.timer = undefined;
-      if (this.callback) this.callback();
-    }, waitMs);
-    _testOnly_allTasks.set(this.timer, this);
+    this.promise = new Promise(resolve => {
+      this.resolvePromise = resolve;
+      this.timerId = window.setTimeout(() => {
+        if (this.timerId) _testOnly_allTasks.delete(this.timerId);
+        this.timerId = undefined;
+        if (this.callback) this.callback();
+        resolve(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
+      }, waitMs);
+      _testOnly_allTasks.set(this.timerId, this);
+    });
+  }
+
+  private cancelTimer() {
+    window.clearTimeout(this.timerId);
+    if (this.timerId) _testOnly_allTasks.delete(this.timerId);
+    this.timerId = undefined;
   }
 
   cancel() {
     if (this.isActive()) {
-      window.clearTimeout(this.timer);
-      if (this.timer) _testOnly_allTasks.delete(this.timer);
-      this.timer = undefined;
+      this.cancelTimer();
+      this.resolvePromise?.(ResolvedDelayedTaskStatus.TASK_CANCELLED);
     }
   }
 
   flush() {
     if (this.isActive()) {
-      this.cancel();
+      this.cancelTimer();
       if (this.callback) this.callback();
+      this.resolvePromise?.(ResolvedDelayedTaskStatus.CALLBACK_EXECUTED);
     }
   }
 
   isActive() {
-    return this.timer !== undefined;
+    return this.timerId !== undefined;
   }
 }
 
diff --git a/polygerrit-ui/app/utils/date-util.ts b/polygerrit-ui/app/utils/date-util.ts
index 72e6cb7..d95d24a 100644
--- a/polygerrit-ui/app/utils/date-util.ts
+++ b/polygerrit-ui/app/utils/date-util.ts
@@ -83,21 +83,17 @@
   return diff < 180 * Duration.DAY;
 }
 
-// TODO(dmfilippov): TS-Fix review this type. All fields here must be optional,
-// but this require some changes in the code. During JS->TS migration
-// we want to avoid code changes where possible, so for simplicity we
-// define it with almost all fields mandatory
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts
 interface DateTimeFormatParts {
-  year: string;
-  month: string;
-  day: string;
-  hour: string;
-  minute: string;
-  second: string;
-  dayPeriod: string;
-  dayperiod?: string;
-  // Object can have other properties, but our code doesn't use it
-  [key: string]: string | undefined;
+  year?: string;
+  month?: string;
+  day?: string;
+  hour?: string;
+  minute?: string;
+  second?: string;
+  // AM or PM
+  dayPeriod?: string;
+  weekday?: string;
 }
 
 export function formatDate(date: Date, format: string) {
@@ -117,6 +113,14 @@
     }
   }
 
+  if (format.includes('ddd')) {
+    if (format.includes('dddd')) {
+      options.weekday = 'long';
+    } else {
+      options.weekday = 'short';
+    }
+  }
+
   if (format.includes('DD')) {
     options.day = '2-digit';
   }
@@ -146,15 +150,38 @@
     locale = 'en-GB';
   }
 
-  const dtf = new Intl.DateTimeFormat(locale, options);
-  const parts = dtf
-    .formatToParts(date)
-    .filter(o => o.type !== 'literal')
-    .reduce((acc, o: Intl.DateTimeFormatPart) => {
-      acc[o.type] = o.value;
-      return acc;
-    }, {} as DateTimeFormatParts);
-  if (format.includes('YY')) {
+  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/formatToParts
+  const dtfParts = new Intl.DateTimeFormat(locale, options).formatToParts(date);
+  const parts: DateTimeFormatParts = {};
+  for (const entry of dtfParts) {
+    switch (entry.type) {
+      case 'year':
+        parts.year = entry.value;
+        break;
+      case 'month':
+        parts.month = entry.value;
+        break;
+      case 'day':
+        parts.day = entry.value;
+        break;
+      case 'hour':
+        parts.hour = entry.value;
+        break;
+      case 'minute':
+        parts.minute = entry.value;
+        break;
+      case 'second':
+        parts.second = entry.value;
+        break;
+      case 'dayPeriod':
+        parts.dayPeriod = entry.value;
+        break;
+      case 'weekday':
+        parts.weekday = entry.value;
+        break;
+    }
+  }
+  if (parts.year && format.includes('YY')) {
     if (format.includes('YYYY')) {
       format = format.replace('YYYY', parts.year);
     } else {
@@ -162,41 +189,50 @@
     }
   }
 
-  if (format.includes('DD')) {
+  if (parts.day && format.includes('DD')) {
     format = format.replace('DD', parts.day);
   }
 
-  if (format.includes('HH')) {
+  if (parts.hour && format.includes('HH')) {
     format = format.replace('HH', parts.hour);
   }
 
-  if (format.includes('h')) {
+  if (parts.hour && format.includes('h')) {
     format = format.replace('h', parts.hour);
   }
 
-  if (format.includes('mm')) {
+  if (parts.minute && format.includes('mm')) {
     format = format.replace('mm', parts.minute);
   }
 
-  if (format.includes('ss')) {
+  if (parts.second && format.includes('ss')) {
     format = format.replace('ss', parts.second);
   }
 
-  if (format.includes('A')) {
-    if (parts.dayperiod) {
-      // Workaround for chrome 70 and below
-      format = format.replace('A', parts.dayperiod.toUpperCase());
-    } else {
-      format = format.replace('A', parts.dayPeriod.toUpperCase());
-    }
+  if (parts.dayPeriod && format.includes('A')) {
+    format = format.replace('A', parts.dayPeriod.toUpperCase());
   }
-  if (format.includes('MM')) {
+
+  // Month and weekday must be last, because they will yield characters that
+  // could be interpreted as format strings, e.g. `h` in `Thursday` would
+  // otherwise be replaced by "hours".
+
+  if (parts.month && format.includes('MM')) {
     if (format.includes('MMM')) {
       format = format.replace('MMM', parts.month);
     } else {
       format = format.replace('MM', parts.month);
     }
   }
+
+  if (parts.weekday && format.includes('ddd')) {
+    if (format.includes('dddd')) {
+      format = format.replace('dddd', parts.weekday);
+    } else {
+      format = format.replace('ddd', parts.weekday);
+    }
+  }
+
   return format;
 }
 
diff --git a/polygerrit-ui/app/utils/date-util_test.ts b/polygerrit-ui/app/utils/date-util_test.ts
index 8e802b7..8d16655 100644
--- a/polygerrit-ui/app/utils/date-util_test.ts
+++ b/polygerrit-ui/app/utils/date-util_test.ts
@@ -194,6 +194,18 @@
         )
       );
     });
+
+    test('weekday', () => {
+      assert.equal(
+        '2013-07-03 Wed',
+        formatDate(new Date('Jul 03 2013 12:14:00'), 'YYYY-MM-DD ddd')
+      );
+      assert.equal(
+        '2013-07-03 Wednesday',
+        formatDate(new Date('Jul 03 2013 00:15:00'), 'YYYY-MM-DD dddd')
+      );
+    });
+
     test('h:mm:ss A shows correctly midnight and midday', () => {
       const timeFormat = 'h:mm A';
       assert.equal(
diff --git a/polygerrit-ui/app/utils/event-util.ts b/polygerrit-ui/app/utils/event-util.ts
index 714955b..49d5382 100644
--- a/polygerrit-ui/app/utils/event-util.ts
+++ b/polygerrit-ui/app/utils/event-util.ts
@@ -103,10 +103,6 @@
   fire(target, EventType.SHOW_TAB, detail);
 }
 
-export function fireCloseFixPreview(target: EventTarget, fixApplied: boolean) {
-  fire(target, EventType.CLOSE_FIX_PREVIEW, {fixApplied});
-}
-
 export function fireReload(target: EventTarget, clearPatchset?: boolean) {
   fire(target, EventType.RELOAD, {clearPatchset: !!clearPatchset});
 }
diff --git a/polygerrit-ui/app/utils/link-util.ts b/polygerrit-ui/app/utils/link-util.ts
index 48e9c07..ccafa5a 100644
--- a/polygerrit-ui/app/utils/link-util.ts
+++ b/polygerrit-ui/app/utils/link-util.ts
@@ -33,6 +33,11 @@
     commentLinkInfo =>
       commentLinkInfo.enabled !== false && commentLinkInfo.link !== undefined
   );
+  // Always linkify URLs starting with https?://
+  enabledRewrites.push({
+    match: '(https?://\\S+[\\w/])',
+    link: '$1',
+  });
   return enabledRewrites.flatMap(rewrite => {
     const regexp = new RegExp(rewrite.match, 'g');
     const partialResults: RewriteResult[] = [];
diff --git a/polygerrit-ui/app/utils/link-util_test.ts b/polygerrit-ui/app/utils/link-util_test.ts
index e4e719b..52b8288 100644
--- a/polygerrit-ui/app/utils/link-util_test.ts
+++ b/polygerrit-ui/app/utils/link-util_test.ts
@@ -12,6 +12,17 @@
   }
 
   suite('link rewrites', () => {
+    test('default linking', () => {
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('http://www.google.com', {}),
+        link('http://www.google.com', 'http://www.google.com')
+      );
+      assert.equal(
+        linkifyUrlsAndApplyRewrite('https://www.google.com', {}),
+        link('https://www.google.com', 'https://www.google.com')
+      );
+    });
+
     test('without text', () => {
       assert.equal(
         linkifyUrlsAndApplyRewrite('foo', {
diff --git a/tools/deps.bzl b/tools/deps.bzl
index 28904a9..4c21ae6 100644
--- a/tools/deps.bzl
+++ b/tools/deps.bzl
@@ -18,7 +18,7 @@
 GITILES_REPO = GERRIT
 
 # When updating Bouncy Castle, also update it in bazlets.
-BC_VERS = "1.64"
+BC_VERS = "1.72"
 HTTPCOMP_VERS = "4.5.2"
 JETTY_VERS = "9.4.49.v20220914"
 BYTE_BUDDY_VERSION = "1.10.7"
@@ -558,20 +558,26 @@
 
     maven_jar(
         name = "bcprov",
-        artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS,
-        sha1 = "1467dac1b787b5ad2a18201c0c281df69882259e",
+        artifact = "org.bouncycastle:bcprov-jdk18on:" + BC_VERS,
+        sha1 = "d8dc62c28a3497d29c93fee3e71c00b27dff41b4",
     )
 
     maven_jar(
         name = "bcpg",
-        artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS,
-        sha1 = "56956a8c63ccadf62e7c678571cf86f30bd84441",
+        artifact = "org.bouncycastle:bcpg-jdk18on:" + BC_VERS,
+        sha1 = "1a36a1740d07869161f6f0d01fae8d72dd1d8320",
     )
 
     maven_jar(
         name = "bcpkix",
-        artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS,
-        sha1 = "3dac163e20110817d850d17e0444852a6d7d0bd7",
+        artifact = "org.bouncycastle:bcpkix-jdk18on:" + BC_VERS,
+        sha1 = "bb3fdb5162ccd5085e8d7e57fada4d8eaa571f5a",
+    )
+
+    maven_jar(
+        name = "bcutil",
+        artifact = "org.bouncycastle:bcutil-jdk18on:" + BC_VERS,
+        sha1 = "41f19a69ada3b06fa48781120d8bebe1ba955c77",
     )
 
     maven_jar(