Merge branch 'stable-3.7'

* stable-3.7:
  Allow to hide download schemes from the REST API and  UI
  Remove documentation of removed option download.maxBundleSize
  TaskThunk#run: ignore SshChannelClosedException
  SSH BaseCommand: handle Throwable instead of Exception
  Always track master on download-commands plugin
  Mark setFunction as deprecated
  Make it possible to run BatchUpdate with non-identified user
  Don't limit PerThreadCache rather limit PerThreadProjectCache
  SSH BaseCommand: handle Throwable instead of Exception
  Set version to 3.7.2-SNAPSHOT
  Set version to 3.7.1
  Set version to 3.6.5-SNAPSHOT
  Set version to 3.6.4
  Make a viewState a state in gr-editor-view
  Fix “Old Patchset” being displayed on current edits
  Fix e2e test compilation failure

Release-Notes: skip
Change-Id: I047edcfb75d2203b0c7fd5a6d26aab1a115b51fe
diff --git a/.gitmodules b/.gitmodules
index e5eef1e..6217b4d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -20,7 +20,7 @@
 [submodule "plugins/download-commands"]
 	path = plugins/download-commands
 	url = ../plugins/download-commands
-	branch = .
+	branch = master
 
 [submodule "plugins/gitiles"]
 	path = plugins/gitiles
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 19445b8..295bd5a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2074,6 +2074,7 @@
   scheme = anon_http
   scheme = anon_git
   scheme = repo
+  hide = ssh
 ----
 
 The download section configures the allowed download methods.
@@ -2142,6 +2143,17 @@
 If `download.scheme` is not specified, SSH, HTTP and Anonymous HTTP
 downloads are allowed.
 
+[[download.hide]]download.hide::
++
+Schemes that can be used to download changes, but will not be advertised
+in the UI. This can be any scheme that can be configured in <<download.scheme>>.
++
+This is mostly useful in a deprecation scenario during a time where using
+a scheme is discouraged, but has to be supported until all clients have
+migrated to use a different scheme.
++
+By default, no scheme will be hidden in the UI.
+
 [[download.checkForHiddenChangeRefs]]download.checkForHiddenChangeRefs::
 +
 Whether the download commands should be adapted when the change refs
@@ -2189,15 +2201,6 @@
 For this reason `zip` format is always excluded from formats offered
 through the `Download` drop down or accessible in the REST API.
 
-[[download.maxBundleSize]]download.maxBundleSize::
-+
-Specifies the maximum size of a bundle in bytes that can be downloaded.
-As bundles are kept in memory this setting is to protect the server
-from a single request consuming too much heap when generating
-a bundle and thereby impacting other users.
-+
-Defaults to 100MB.
-
 [[gc]]
 === Section gc
 
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala
index 53f942d..5885fb0 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/GitSimulation.scala
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.scenarios
 
-import static java.nio.charset.StandardCharsets.UTF_8
+import java.nio.charset.StandardCharsets.UTF_8
 import java.io.{File, IOException}
 import java.net.URLEncoder
 
diff --git a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ProjectSimulation.scala b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ProjectSimulation.scala
index f0c6f68..3802cea 100644
--- a/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ProjectSimulation.scala
+++ b/e2e-tests/src/test/scala/com/google/gerrit/scenarios/ProjectSimulation.scala
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.scenarios
 
-import static java.nio.charset.StandardCharsets.UTF_8
+import java.nio.charset.StandardCharsets.UTF_8
 import java.net.URLEncoder
 
 class ProjectSimulation extends GerritSimulation {
   projectName = "defaultTestProject"
 
   override def replaceOverride(in: String): String = {
-    replaceProperty("project", URLEncoder.encode(getFullProjectName(projectName), UTF_8, in)
+    replaceProperty("project", URLEncoder.encode(getFullProjectName(projectName), UTF_8), in)
   }
 }
diff --git a/java/com/google/gerrit/entities/LabelType.java b/java/com/google/gerrit/entities/LabelType.java
index f009872..7a3266ecc 100644
--- a/java/com/google/gerrit/entities/LabelType.java
+++ b/java/com/google/gerrit/entities/LabelType.java
@@ -203,6 +203,11 @@
 
     public abstract Builder setDescription(Optional<String> description);
 
+    /**
+     * @deprecated in favour of using submit requirements, except if it’s needed to set the value to
+     *     PatchSetLock
+     */
+    @Deprecated
     public abstract Builder setFunction(LabelFunction function);
 
     public abstract Builder setCanOverride(boolean canOverride);
diff --git a/java/com/google/gerrit/extensions/config/DownloadScheme.java b/java/com/google/gerrit/extensions/config/DownloadScheme.java
index 96b5878..15801d4 100644
--- a/java/com/google/gerrit/extensions/config/DownloadScheme.java
+++ b/java/com/google/gerrit/extensions/config/DownloadScheme.java
@@ -34,4 +34,7 @@
 
   /** Returns whether the download scheme is enabled */
   public abstract boolean isEnabled();
+
+  /** Returns whether the download scheme is hidden in the UI */
+  public abstract boolean isHidden();
 }
diff --git a/java/com/google/gerrit/server/cache/PerThreadCache.java b/java/com/google/gerrit/server/cache/PerThreadCache.java
index ef00b80..8ae9710 100644
--- a/java/com/google/gerrit/server/cache/PerThreadCache.java
+++ b/java/com/google/gerrit/server/cache/PerThreadCache.java
@@ -43,19 +43,9 @@
  * <p>Lastly, this class offers a cache, that requires callers to also provide a {@code Supplier} in
  * case the object is not present in the cache, while {@code CurrentUser} provides a storage where
  * just retrieving stored values is a valid operation.
- *
- * <p>To prevent OOM errors on requests that would cache a lot of objects, this class enforces an
- * internal limit after which no new elements are cached. All {@code get} calls are served by
- * invoking the {@code Supplier} after that.
  */
 public class PerThreadCache implements AutoCloseable {
   private static final ThreadLocal<PerThreadCache> CACHE = new ThreadLocal<>();
-  /**
-   * Cache at maximum 25 values per thread. This value was chosen arbitrarily. Some endpoints (like
-   * ListProjects) break the assumption that the data cached in a request is limited. To prevent
-   * this class from accumulating an unbound number of objects, we enforce this limit.
-   */
-  private static final int PER_THREAD_CACHE_SIZE = 25;
 
   /**
    * Unique key for key-value mappings stored in PerThreadCache. The key is based on the value's
@@ -119,7 +109,7 @@
     return cache != null ? cache.get(key, loader) : loader.get();
   }
 
-  private final Map<Key<?>, Object> cache = Maps.newHashMapWithExpectedSize(PER_THREAD_CACHE_SIZE);
+  private final Map<Key<?>, Object> cache = Maps.newHashMap();
 
   private PerThreadCache() {}
 
@@ -132,9 +122,7 @@
     T value = (T) cache.get(key);
     if (value == null) {
       value = loader.get();
-      if (cache.size() < PER_THREAD_CACHE_SIZE) {
-        cache.put(key, value);
-      }
+      cache.put(key, value);
     }
     return value;
   }
diff --git a/java/com/google/gerrit/server/cache/PerThreadProjectCache.java b/java/com/google/gerrit/server/cache/PerThreadProjectCache.java
new file mode 100644
index 0000000..86f1d2d
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/PerThreadProjectCache.java
@@ -0,0 +1,64 @@
+// 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.cache;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.entities.Project;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * To prevent OOM errors on requests that would cache a lot of objects, this class enforces an
+ * internal limit after which no new elements are cached. All {@code computeIfAbsentWithinLimit}
+ * calls are served by invoking the {@code Supplier} after that.
+ */
+public class PerThreadProjectCache {
+  private static final PerThreadCache.Key<PerThreadProjectCache> PER_THREAD_PROJECT_CACHE_KEY =
+      PerThreadCache.Key.create(PerThreadProjectCache.class);
+  /**
+   * Cache at maximum 25 values per thread. This value was chosen arbitrarily. Some endpoints (like
+   * ListProjects) break the assumption that the data cached in a request is limited. To prevent
+   * this class from accumulating an unbound number of objects, we enforce this limit.
+   */
+  private static final int PER_THREAD_PROJECT_CACHE_SIZE = 25;
+
+  private final Map<PerThreadCache.Key<Project.NameKey>, Object> valueByNameKey =
+      Maps.newHashMapWithExpectedSize(PER_THREAD_PROJECT_CACHE_SIZE);
+
+  private PerThreadProjectCache() {}
+
+  public static <T> T getOrCompute(PerThreadCache.Key<Project.NameKey> key, Supplier<T> loader) {
+    PerThreadCache perThreadCache = PerThreadCache.get();
+    if (perThreadCache != null) {
+      PerThreadProjectCache perThreadProjectCache =
+          perThreadCache.get(PER_THREAD_PROJECT_CACHE_KEY, PerThreadProjectCache::new);
+      return perThreadProjectCache.computeIfAbsentWithinLimit(key, loader);
+    }
+    return loader.get();
+  }
+
+  protected <T> T computeIfAbsentWithinLimit(
+      PerThreadCache.Key<Project.NameKey> key, Supplier<T> loader) {
+    @SuppressWarnings("unchecked")
+    T value = (T) valueByNameKey.get(key);
+    if (value == null) {
+      value = loader.get();
+      if (valueByNameKey.size() < PER_THREAD_PROJECT_CACHE_SIZE) {
+        valueByNameKey.put(key, value);
+      }
+    }
+    return value;
+  }
+}
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index ce63c7e..c4fd5be 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -250,6 +250,7 @@
       String schemeName = e.getExportName();
       DownloadScheme scheme = e.getProvider().get();
       if (!scheme.isEnabled()
+          || scheme.isHidden()
           || (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) {
         continue;
       }
diff --git a/java/com/google/gerrit/server/config/DownloadConfig.java b/java/com/google/gerrit/server/config/DownloadConfig.java
index d581675..8ac858c 100644
--- a/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -24,8 +25,11 @@
 import com.google.inject.Singleton;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.util.Arrays;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 
 /**
@@ -39,10 +43,12 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   private final ImmutableSet<String> downloadSchemes;
+  private final ImmutableSet<String> hiddenSchemes;
   private final ImmutableSet<DownloadCommand> downloadCommands;
   private final ImmutableSet<ArchiveFormatInternal> archiveFormats;
 
   @Inject
+  @VisibleForTesting
   public DownloadConfig(@GerritServerConfig Config cfg) {
     String[] allSchemes = cfg.getStringList("download", null, "scheme");
     if (allSchemes.length == 0) {
@@ -63,6 +69,10 @@
       downloadSchemes = normalized.build();
     }
 
+    Set<String> hidden = new HashSet<>(Arrays.asList(cfg.getStringList("download", null, "hide")));
+    hidden.retainAll(downloadSchemes);
+    hiddenSchemes = ImmutableSet.copyOf(hidden);
+
     DownloadCommand[] downloadCommandValues = DownloadCommand.values();
     List<DownloadCommand> allCommands =
         ConfigUtil.getEnumList(cfg, "download", null, "command", downloadCommandValues, null);
@@ -110,6 +120,11 @@
     return downloadSchemes;
   }
 
+  /** Scheme hidden in the UI. */
+  public ImmutableSet<String> getHiddenSchemes() {
+    return hiddenSchemes;
+  }
+
   /** Command used to download. */
   public ImmutableSet<DownloadCommand> getDownloadCommands() {
     return downloadCommands;
diff --git a/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java b/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
index fde4088..6a73348 100644
--- a/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
+++ b/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -28,7 +29,7 @@
   private final NotifyHandling notify;
 
   protected AbstractChangeEvent(
-      ChangeInfo change, AccountInfo who, Instant when, NotifyHandling notify) {
+      ChangeInfo change, @Nullable AccountInfo who, Instant when, NotifyHandling notify) {
     this.changeInfo = change;
     this.who = who;
     this.when = when;
diff --git a/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java b/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
index 27e0a5e..04ffcc1 100644
--- a/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
+++ b/java/com/google/gerrit/server/extensions/events/AttentionSetObserver.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.extensions.events;
 
 import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AttentionSetUpdate;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
@@ -73,7 +74,10 @@
    * @param when is the time of the event
    */
   public void fire(
-      ChangeData changeData, AccountState accountState, AttentionSetUpdate update, Instant when) {
+      ChangeData changeData,
+      @Nullable AccountState accountState,
+      AttentionSetUpdate update,
+      Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -107,7 +111,7 @@
 
     public Event(
         ChangeInfo change,
-        AccountInfo editor,
+        @Nullable AccountInfo editor,
         Set<Integer> added,
         Set<Integer> removed,
         Instant when) {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index aa49852..bf4d05a 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.cache.PerThreadCache;
+import com.google.gerrit.server.cache.PerThreadProjectCache;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -124,8 +125,8 @@
     public ForProject project(Project.NameKey project) {
       try {
         ProjectControl control =
-            PerThreadCache.getOrCompute(
-                PerThreadCache.Key.create(ProjectControl.class, project, user.getCacheKey()),
+            PerThreadProjectCache.getOrCompute(
+                PerThreadCache.Key.create(Project.NameKey.class, project, user.getCacheKey()),
                 () ->
                     projectControlFactory.create(
                         user, projectCache.get(project).orElseThrow(illegalState(project))));
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index e8a6671..977c737 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -231,7 +231,7 @@
     downloadSchemes.runEach(
         extension -> {
           DownloadScheme scheme = extension.get();
-          if (scheme.isEnabled() && scheme.getUrl("${project}") != null) {
+          if (scheme.isEnabled() && !scheme.isHidden() && scheme.getUrl("${project}") != null) {
             info.schemes.put(extension.getExportName(), getDownloadSchemeInfo(scheme));
           }
         });
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index c1c58c8..547aff3 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -55,6 +55,7 @@
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.channel.exception.SshChannelClosedException;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
 import org.apache.sshd.server.channel.ChannelSession;
@@ -499,19 +500,11 @@
             throw new UnloggedFailure(1, e.getMessage() + " no such change");
           }
 
-          out.flush();
-          err.flush();
-        } catch (Exception e) {
-          try {
-            out.flush();
-          } catch (Exception e2) {
-            // Ignored
-          }
-          try {
-            err.flush();
-          } catch (Exception e2) {
-            // Ignored
-          }
+          flushIgnoreSCCE(out);
+          flushIgnoreSCCE(err);
+        } catch (Throwable e) {
+          flushIgnoreException(out);
+          flushIgnoreException(err);
           rc = handleError(e);
         } finally {
           try {
@@ -524,6 +517,22 @@
       }
     }
 
+    private void flushIgnoreSCCE(OutputStream os) throws IOException {
+      try {
+        os.flush();
+      } catch (SshChannelClosedException e) {
+        // Ignore - command implementation flushed stream already
+      }
+    }
+
+    private void flushIgnoreException(OutputStream os) {
+      try {
+        os.flush();
+      } catch (Exception e) {
+        // Ignore
+      }
+    }
+
     @Override
     public String toString() {
       return taskName;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index 5745a4e..59ba00b 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -247,5 +247,10 @@
     public boolean isEnabled() {
       return true;
     }
+
+    @Override
+    public boolean isHidden() {
+      return false;
+    }
   }
 }
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
index 275f2ec..cd6a6b4 100644
--- a/javatests/com/google/gerrit/server/cache/BUILD
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -5,6 +5,7 @@
     name = "tests",
     srcs = glob(["*Test.java"]),
     deps = [
+        "//java/com/google/gerrit/entities",
         "//java/com/google/gerrit/server",
         "//java/com/google/gerrit/testing:gerrit-test-util",
         "//lib:junit",
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
index 5d420d3..6966302 100644
--- a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
+++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
@@ -84,20 +84,4 @@
       assertThat(thrown).hasMessageThat().contains("called create() twice on the same request");
     }
   }
-
-  @Test
-  public void enforceMaxSize() {
-    try (PerThreadCache cache = PerThreadCache.create()) {
-      // Fill the cache
-      for (int i = 0; i < 50; i++) {
-        PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, i);
-        cache.get(key, () -> "cached value");
-      }
-      // Assert that the value was not persisted
-      PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, 1000);
-      cache.get(key, () -> "new value");
-      String value = cache.get(key, () -> "directly served");
-      assertThat(value).isEqualTo("directly served");
-    }
-  }
 }
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadProjectCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadProjectCacheTest.java
new file mode 100644
index 0000000..055b95d
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/PerThreadProjectCacheTest.java
@@ -0,0 +1,51 @@
+// 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.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.entities.Project;
+import org.junit.Test;
+
+public class PerThreadProjectCacheTest {
+  @Test
+  public void testValueIsCachedWithinSizeLimit() {
+    try (PerThreadCache cache = PerThreadCache.create()) {
+      PerThreadCache.Key<Project.NameKey> key =
+          PerThreadCache.Key.create(Project.NameKey.class, Project.nameKey("test-project"));
+      PerThreadProjectCache.getOrCompute(key, () -> "cached");
+      String value = PerThreadProjectCache.getOrCompute(key, () -> "directly served");
+      assertThat(value).isEqualTo("cached");
+    }
+  }
+
+  @Test
+  public void testEnforceMaxSize() {
+    try (PerThreadCache cache = PerThreadCache.create()) {
+      // Fill the cache
+      for (int i = 0; i < 50; i++) {
+        PerThreadCache.Key<Project.NameKey> key =
+            PerThreadCache.Key.create(Project.NameKey.class, Project.nameKey("test-project" + i));
+        PerThreadProjectCache.getOrCompute(key, () -> "cached");
+      }
+      // Assert that the value was not persisted
+      PerThreadCache.Key<Project.NameKey> key =
+          PerThreadCache.Key.create(Project.NameKey.class, "Project" + 1000);
+      PerThreadProjectCache.getOrCompute(key, () -> "new value");
+      String value = PerThreadProjectCache.getOrCompute(key, () -> "directly served");
+      assertThat(value).isEqualTo("directly served");
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index a49f156..ff1f6a3 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -39,8 +39,10 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.change.AbandonOp;
 import com.google.gerrit.server.change.AddReviewersOp;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.PatchSetInserter;
@@ -103,6 +105,8 @@
   @Inject private AccountManager accountManager;
   @Inject private AuthRequest.Factory authRequestFactory;
   @Inject private IdentifiedUser.GenericFactory userFactory;
+  @Inject private InternalUser.Factory internalUserFactory;
+  @Inject private AbandonOp.Factory abandonOpFactory;
 
   @Rule public final MockitoRule mockito = MockitoJUnit.rule();
 
@@ -152,6 +156,40 @@
   }
 
   @Test
+  public void batchUpdateThatChangeAttentionSetAsInternalUser() throws Exception {
+    Change.Id id = createChangeWithUpdates(1);
+    attentionSetListeners.add("test", attentionSetListener);
+
+    Account.Id reviewer =
+        accountManager.authenticate(authRequestFactory.createForUser("user")).getAccountId();
+
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
+      bu.addOp(
+          id,
+          addReviewersOpFactory.create(
+              ImmutableSet.of(reviewer), ImmutableList.of(), ReviewerState.REVIEWER, false));
+      bu.execute();
+    }
+
+    try (BatchUpdate bu =
+        batchUpdateFactory.create(project, internalUserFactory.create(), TimeUtil.now())) {
+      bu.addOp(id, abandonOpFactory.create(null, "test abandon"));
+      bu.execute();
+    }
+
+    verify(attentionSetListener, times(2)).onAttentionSetChanged(attentionSetEventCaptor.capture());
+    AttentionSetListener.Event event = attentionSetEventCaptor.getAllValues().get(0);
+    assertThat(event.getChange()._number).isEqualTo(id.get());
+    assertThat(event.usersAdded()).containsExactly(reviewer.get());
+    assertThat(event.usersRemoved()).isEmpty();
+
+    event = attentionSetEventCaptor.getAllValues().get(1);
+    assertThat(event.getChange()._number).isEqualTo(id.get());
+    assertThat(event.usersAdded()).isEmpty();
+    assertThat(event.usersRemoved()).containsExactly(reviewer.get());
+  }
+
+  @Test
   public void cannotExceedMaxUpdates() throws Exception {
     Change.Id id = createChangeWithUpdates(MAX_UPDATES);
     ObjectId oldMetaId = getMetaId(id);
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 1ebf3d0..b923e44 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.7.1-SNAPSHOT</version>
+  <version>3.7.2-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 5d817af..b3110c3 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.7.1-SNAPSHOT</version>
+  <version>3.7.2-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index bc61907c..345d80a 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.7.1-SNAPSHOT</version>
+  <version>3.7.2-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 77c1134..9e30306 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.7.1-SNAPSHOT</version>
+  <version>3.7.2-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/version.bzl b/version.bzl
index 569943f..120e839 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.7.1-SNAPSHOT"
+GERRIT_VERSION = "3.7.2-SNAPSHOT"