Merge "Show "Loading..." while the autocomplete query is loading."
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 75d8847..2810d1e 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -6709,6 +6709,7 @@
 |`patch`             |required|
 The patch to be applied. Must be compatible with `git diff` output.
 For example, link:#get-patch[Get Patch] output.
+The patch must be provided as UTF-8 text, either directly or base64-encoded.
 |=================================
 
 [[applypatchpatchset-input]]
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index a149f29..36bc3c4 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -67,6 +67,7 @@
 import com.google.gerrit.server.util.ReplicaUtil;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
+import com.google.gerrit.testing.FakeAccountPatchReviewStore.FakeAccountPatchReviewStoreModule;
 import com.google.gerrit.testing.FakeEmailSender.FakeEmailSenderModule;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.SshMode;
@@ -414,6 +415,7 @@
               }
             },
             site);
+    daemon.setAccountPatchReviewStoreModuleForTesting(new FakeAccountPatchReviewStoreModule());
     daemon.setEmailModuleForTesting(new FakeEmailSenderModule());
     daemon.setAuditEventModuleForTesting(
         MoreObjects.firstNonNull(testAuditModule, new FakeGroupAuditServiceModule()));
diff --git a/java/com/google/gerrit/acceptance/TestMetricMaker.java b/java/com/google/gerrit/acceptance/TestMetricMaker.java
index 85c5b6d..647eb9d 100644
--- a/java/com/google/gerrit/acceptance/TestMetricMaker.java
+++ b/java/com/google/gerrit/acceptance/TestMetricMaker.java
@@ -14,20 +14,26 @@
 
 package com.google.gerrit.acceptance;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
 import com.google.gerrit.metrics.Description;
 import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.Field;
 import com.google.inject.Singleton;
+import java.util.Arrays;
 import java.util.concurrent.ConcurrentHashMap;
 import org.apache.commons.lang3.mutable.MutableLong;
 
 /**
  * {@link com.google.gerrit.metrics.MetricMaker} to be bound in tests.
  *
- * <p>Records how often {@link Counter0} metrics are invoked. Metrics of other types are not
- * recorded.
+ * <p>Records how often counter metrics are invoked. Metrics of other types are not recorded.
  *
- * <p>Allows test to check how much a {@link Counter0} metrics is increased by an operation.
+ * <p>Allows test to check how much a counter metrics is increased by an operation.
  *
  * <p>Example:
  *
@@ -48,18 +54,18 @@
  */
 @Singleton
 public class TestMetricMaker extends DisabledMetricMaker {
-  private final ConcurrentHashMap<String, MutableLong> counts = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap<CounterKey, MutableLong> counts = new ConcurrentHashMap<>();
 
-  public long getCount(String counter0Name) {
-    return get(counter0Name).longValue();
+  public long getCount(String counterName, Object... fieldValues) {
+    return get(CounterKey.create(counterName, fieldValues)).longValue();
   }
 
   public void reset() {
     counts.clear();
   }
 
-  private MutableLong get(String counter0Name) {
-    return counts.computeIfAbsent(counter0Name, name -> new MutableLong(0));
+  private MutableLong get(CounterKey counterKey) {
+    return counts.computeIfAbsent(counterKey, key -> new MutableLong(0));
   }
 
   @Override
@@ -67,11 +73,64 @@
     return new Counter0() {
       @Override
       public void incrementBy(long value) {
-        get(name).add(value);
+        get(CounterKey.create(name)).add(value);
       }
 
       @Override
       public void remove() {}
     };
   }
+
+  @Override
+  public <F1> Counter1<F1> newCounter(String name, Description desc, Field<F1> field1) {
+    return new Counter1<>() {
+      @Override
+      public void incrementBy(F1 field1, long value) {
+        get(CounterKey.create(name, field1)).add(value);
+      }
+
+      @Override
+      public void remove() {}
+    };
+  }
+
+  @Override
+  public <F1, F2> Counter2<F1, F2> newCounter(
+      String name, Description desc, Field<F1> field1, Field<F2> field2) {
+    return new Counter2<>() {
+      @Override
+      public void incrementBy(F1 field1, F2 field2, long value) {
+        get(CounterKey.create(name, field1, field2)).add(value);
+      }
+
+      @Override
+      public void remove() {}
+    };
+  }
+
+  @Override
+  public <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
+      String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+    return new Counter3<>() {
+      @Override
+      public void incrementBy(F1 field1, F2 field2, F3 field3, long value) {
+        get(CounterKey.create(name, field1, field2, field3)).add(value);
+      }
+
+      @Override
+      public void remove() {}
+    };
+  }
+
+  @AutoValue
+  abstract static class CounterKey {
+    abstract String name();
+
+    abstract ImmutableList<Object> fieldValues();
+
+    static CounterKey create(String name, Object... fieldValues) {
+      return new AutoValue_TestMetricMaker_CounterKey(
+          name, ImmutableList.copyOf(Arrays.asList(fieldValues)));
+    }
+  }
 }
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 845cc9a..744f91b 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -215,6 +215,7 @@
   private Path runFile;
   private boolean inMemoryTest;
   private AbstractModule indexModule;
+  private Module accountPatchReviewStoreModule;
   private Module emailModule;
   private List<Module> testSysModules = new ArrayList<>();
   private List<Module> testSshModules = new ArrayList<>();
@@ -333,6 +334,11 @@
   }
 
   @VisibleForTesting
+  public void setAccountPatchReviewStoreModuleForTesting(Module module) {
+    accountPatchReviewStoreModule = module;
+  }
+
+  @VisibleForTesting
   public void setEmailModuleForTesting(Module module) {
     emailModule = module;
   }
@@ -442,7 +448,11 @@
     modules.add(new WorkQueueModule());
     modules.add(new StreamEventsApiListenerModule());
     modules.add(new EventBrokerModule());
-    modules.add(new JdbcAccountPatchReviewStoreModule(config));
+    if (accountPatchReviewStoreModule != null) {
+      modules.add(accountPatchReviewStoreModule);
+    } else {
+      modules.add(new JdbcAccountPatchReviewStoreModule(config));
+    }
     modules.add(new SysExecutorModule());
     modules.add(new DiffExecutorModule());
     modules.add(new MimeUtil2Module());
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/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/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index fb9e64e..81a6443 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -51,6 +51,7 @@
         "//lib:junit",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
+        "//lib/errorprone:annotations",
         "//lib/flogger:api",
         "//lib/guice",
         "//lib/guice:guice-servlet",
diff --git a/java/com/google/gerrit/testing/FakeAccountPatchReviewStore.java b/java/com/google/gerrit/testing/FakeAccountPatchReviewStore.java
new file mode 100644
index 0000000..1533aeb
--- /dev/null
+++ b/java/com/google/gerrit/testing/FakeAccountPatchReviewStore.java
@@ -0,0 +1,141 @@
+// 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.auto.value.AutoValue;
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.change.AccountPatchReviewStore;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * An implementation of the {@link AccountPatchReviewStore} that's only used in tests. This
+ * implementation stores reviewed files in memory.
+ */
+@Singleton
+public class FakeAccountPatchReviewStore implements AccountPatchReviewStore, LifecycleListener {
+
+  private final Set<Entity> store = new HashSet<>();
+
+  @Override
+  public void start() {}
+
+  @Override
+  public void stop() {}
+
+  public static class FakeAccountPatchReviewStoreModule extends LifecycleModule {
+    @Override
+    protected void configure() {
+      DynamicItem.bind(binder(), AccountPatchReviewStore.class)
+          .to(FakeAccountPatchReviewStore.class);
+      listener().to(FakeAccountPatchReviewStore.class);
+    }
+  }
+
+  @AutoValue
+  abstract static class Entity {
+    abstract PatchSet.Id psId();
+
+    abstract Account.Id accountId();
+
+    abstract String path();
+
+    static Entity create(PatchSet.Id psId, Account.Id accountId, String path) {
+      return new AutoValue_FakeAccountPatchReviewStore_Entity(psId, accountId, path);
+    }
+  }
+
+  @Override
+  @CanIgnoreReturnValue
+  public boolean markReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
+    synchronized (store) {
+      Entity entity = Entity.create(psId, accountId, path);
+      return store.add(entity);
+    }
+  }
+
+  @Override
+  public void markReviewed(PatchSet.Id psId, Account.Id accountId, Collection<String> paths) {
+    paths.forEach(path -> markReviewed(psId, accountId, path));
+  }
+
+  @Override
+  public void clearReviewed(PatchSet.Id psId, Account.Id accountId, String path) {
+    synchronized (store) {
+      store.remove(Entity.create(psId, accountId, path));
+    }
+  }
+
+  @Override
+  public void clearReviewed(PatchSet.Id psId) {
+    synchronized (store) {
+      List<Entity> toRemove = new ArrayList<>();
+      for (Entity entity : store) {
+        if (entity.psId().equals(psId)) {
+          toRemove.add(entity);
+        }
+      }
+      store.removeAll(toRemove);
+    }
+  }
+
+  @Override
+  public void clearReviewed(Change.Id changeId) {
+    synchronized (store) {
+      List<Entity> toRemove = new ArrayList<>();
+      for (Entity entity : store) {
+        if (entity.psId().changeId().equals(changeId)) {
+          toRemove.add(entity);
+        }
+      }
+      store.removeAll(toRemove);
+    }
+  }
+
+  @Override
+  public Optional<PatchSetWithReviewedFiles> findReviewed(PatchSet.Id psId, Account.Id accountId) {
+    synchronized (store) {
+      int matchedPsNumber = -1;
+      Optional<PatchSetWithReviewedFiles> result = Optional.empty();
+      for (Entity entity : store) {
+        if (entity.accountId() != accountId || !entity.psId().changeId().equals(psId.changeId())) {
+          continue;
+        }
+        int entityPsNumber = Integer.parseInt(entity.psId().getId());
+        if (entityPsNumber <= psId.get() && entityPsNumber > matchedPsNumber) {
+          matchedPsNumber = entityPsNumber;
+          result =
+              Optional.of(
+                  PatchSetWithReviewedFiles.create(
+                      PatchSet.id(psId.changeId(), matchedPsNumber),
+                      ImmutableSet.of(entity.path())));
+        }
+      }
+      return result;
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/TestMetricMakerTest.java b/javatests/com/google/gerrit/acceptance/TestMetricMakerTest.java
new file mode 100644
index 0000000..3464d21
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/TestMetricMakerTest.java
@@ -0,0 +1,202 @@
+// 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.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Tests for {@link TestMetricMaker}. */
+public class TestMetricMakerTest {
+  private TestMetricMaker testMetricMaker = new TestMetricMaker();
+
+  @Before
+  public void setUp() {
+    testMetricMaker.reset();
+  }
+
+  @Test
+  public void counter0() throws Exception {
+    String counterName = "test_counter";
+    Counter0 counter = testMetricMaker.newCounter(counterName, new Description("Test Counter"));
+    assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+
+    counter.increment();
+    assertThat(testMetricMaker.getCount(counterName)).isEqualTo(1);
+
+    counter.incrementBy(/* value= */ 3);
+    assertThat(testMetricMaker.getCount(counterName)).isEqualTo(4);
+  }
+
+  @Test
+  public void counter1_booleanField() throws Exception {
+    String counterName = "test_counter";
+    Counter1<Boolean> counter =
+        testMetricMaker.newCounter(
+            counterName,
+            new Description("Test Counter"),
+            Field.ofBoolean("boolean_field", (metadataBuilder, booleanField) -> {}).build());
+    assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+
+    counter.increment(/* field1= */ true);
+    assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(1);
+    assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+
+    counter.incrementBy(/* field1= */ true, /* value= */ 3);
+    assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+
+    counter.increment(/* field1= */ false);
+    assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(1);
+
+    counter.incrementBy(/* field1= */ false, /* value= */ 4);
+    assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(5);
+
+    assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+  }
+
+  @Test
+  public void counter1_stringField() throws Exception {
+    String counterName = "test_counter";
+    Counter1<String> counter =
+        testMetricMaker.newCounter(
+            counterName,
+            new Description("Test Counter"),
+            Field.ofString("string_field", (metadataBuilder, stringField) -> {}).build());
+    assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(0);
+
+    counter.increment(/* field1= */ "foo");
+    assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(1);
+    assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(0);
+
+    counter.incrementBy(/* field1= */ "foo", /* value= */ 3);
+    assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(0);
+
+    counter.increment(/* field1= */ "bar");
+    assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(1);
+
+    counter.incrementBy(/* field1= */ "bar", /* value= */ 4);
+    assertThat(testMetricMaker.getCount(counterName, "foo")).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, "bar")).isEqualTo(5);
+
+    assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+  }
+
+  @Test
+  public void counter2() throws Exception {
+    String counterName = "test_counter";
+    Counter2<Boolean, String> counter =
+        testMetricMaker.newCounter(
+            counterName,
+            new Description("Test Counter"),
+            Field.ofBoolean("boolean_field", (metadataBuilder, booleanField) -> {}).build(),
+            Field.ofString("string_field", (metadataBuilder, stringField) -> {}).build());
+    assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(0);
+
+    counter.increment(/* field1= */ true, /* field2= */ "foo");
+    assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(1);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(0);
+
+    counter.incrementBy(/* field1= */ true, /* field2= */ "foo", /* value= */ 3);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(0);
+
+    counter.increment(/* field1= */ false, /* field2= */ "foo");
+    assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(1);
+
+    counter.incrementBy(/* field1= */ false, /* field2= */ "foo", /* value= */ 4);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(5);
+
+    counter.increment(/* field1= */ true, /* field2= */ "bar");
+    assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, true, "bar")).isEqualTo(1);
+
+    counter.incrementBy(/* field1= */ true, /* field2= */ "bar", /* value= */ 5);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, true, "bar")).isEqualTo(6);
+
+    assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+  }
+
+  @Test
+  public void counter3() throws Exception {
+    String counterName = "test_counter";
+    Counter3<Boolean, String, Integer> counter =
+        testMetricMaker.newCounter(
+            counterName,
+            new Description("Test Counter"),
+            Field.ofBoolean("boolean_field", (metadataBuilder, booleanField) -> {}).build(),
+            Field.ofString("string_field", (metadataBuilder, stringField) -> {}).build(),
+            Field.ofInteger("integer_field", (metadataBuilder, stringField) -> {}).build());
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(0);
+
+    counter.increment(/* field1= */ true, /* field2= */ "foo", /* field3= */ 0);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(1);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(0);
+
+    counter.incrementBy(/* field1= */ true, /* field2= */ "foo", /* field3= */ 0, /* value= */ 3);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(0);
+
+    counter.increment(/* field1= */ false, /* field2= */ "foo", /* field3= */ 0);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(1);
+
+    counter.incrementBy(/* field1= */ false, /* field2= */ "foo", /* field3= */ 0, /* value= */ 4);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo", 0)).isEqualTo(5);
+
+    counter.increment(/* field1= */ true, /* field2= */ "bar", /* field3= */ 0);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, true, "bar", 0)).isEqualTo(1);
+
+    counter.incrementBy(/* field1= */ true, /* field2= */ "bar", /* field3= */ 0, /* value= */ 5);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, true, "bar", 0)).isEqualTo(6);
+
+    counter.increment(/* field1= */ false, /* field2= */ "foo", /* field3= */ 1);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo", 1)).isEqualTo(1);
+
+    counter.incrementBy(/* field1= */ false, /* field2= */ "foo", /* field3= */ 1, /* value= */ 6);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo", 0)).isEqualTo(4);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo", 1)).isEqualTo(7);
+
+    assertThat(testMetricMaker.getCount(counterName)).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, true)).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, false)).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, true, "foo")).isEqualTo(0);
+    assertThat(testMetricMaker.getCount(counterName, false, "foo")).isEqualTo(0);
+  }
+}
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/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 721d650..bb27237 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
@@ -1244,7 +1244,6 @@
       <div class="changeStatuses">
         ${this.changeStatuses.map(
           status => html` <gr-change-status
-            .change=${this.change}
             .revertedChange=${this.revertedChange}
             .status=${status}
             .resolveWeblinks=${resolveWeblinks}
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
index d2b9e2d..073d9f1 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.ts
@@ -7,7 +7,6 @@
 import '../gr-tooltip-content/gr-tooltip-content';
 import '../../../styles/shared-styles';
 import {ChangeInfo} from '../../../types/common';
-import {ParsedChangeInfo} from '../../../types/types';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {LitElement, PropertyValues, html, css} from 'lit';
 import {customElement, property, state} from 'lit/decorators.js';
@@ -50,9 +49,6 @@
   @property({type: Boolean, reflect: true})
   flat = false;
 
-  @property({type: Object})
-  change?: ChangeInfo | ParsedChangeInfo;
-
   @property({type: String})
   status?: ChangeStates;
 
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/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,