Merge "Merge branch 'stable-3.10'"
diff --git a/Documentation/linux-quickstart.txt b/Documentation/linux-quickstart.txt
index 13873ed..67f0565 100644
--- a/Documentation/linux-quickstart.txt
+++ b/Documentation/linux-quickstart.txt
@@ -29,10 +29,10 @@
 . Download the desired Gerrit archive.
 
 To view previous archives, see
-link:https://gerrit-releases.storage.googleapis.com/index.html[Gerrit Code Review: Releases,role=external,window=_blank]. The steps below install Gerrit 3.5.1:
+link:https://gerrit-releases.storage.googleapis.com/index.html[Gerrit Code Review: Releases,role=external,window=_blank]. The steps below install Gerrit 3.9.4:
 
 ....
-wget https://gerrit-releases.storage.googleapis.com/gerrit-3.5.1.war
+wget https://gerrit-releases.storage.googleapis.com/gerrit-3.9.4.war
 ....
 
 NOTE: To build and install Gerrit from the source files, see
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index ddfee38..6b19906 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -61,6 +61,24 @@
 class H2CacheFactory extends PersistentCacheBaseFactory implements LifecycleListener {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  static class PeriodicCachePruner implements Runnable {
+    private final H2CacheImpl<?, ?> cache;
+
+    PeriodicCachePruner(H2CacheImpl<?, ?> cache) {
+      this.cache = cache;
+    }
+
+    @Override
+    public String toString() {
+      return "Disk Cache Pruner (" + cache.getCacheName() + ")";
+    }
+
+    @Override
+    public void run() {
+      cache.prune();
+    }
+  }
+
   private final List<H2CacheImpl<?, ?>> caches;
   private final DynamicMap<Cache<?, ?>> cacheMap;
   private final ExecutorService executor;
@@ -118,13 +136,13 @@
           if (pruneOnStartup) {
             @SuppressWarnings("unused")
             Future<?> possiblyIgnoredError =
-                cleanup.schedule(() -> cache.prune(), 30, TimeUnit.SECONDS);
+                cleanup.schedule(new PeriodicCachePruner(cache), 30, TimeUnit.SECONDS);
           }
 
           @SuppressWarnings("unused")
           Future<?> possiblyIgnoredError =
               cleanup.scheduleAtFixedRate(
-                  () -> cache.prune(),
+                  new PeriodicCachePruner(cache),
                   schedule.initialDelay(),
                   schedule.interval(),
                   TimeUnit.MILLISECONDS);
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index a869946..c31f58a 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -234,6 +234,10 @@
     logger.atFine().log("Finished pruning cache %s...", cacheName);
   }
 
+  String getCacheName() {
+    return cacheName;
+  }
+
   static class ValueHolder<V> {
     final V value;
     final Instant created;
diff --git a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
index 7f73cd3..ec376e0 100644
--- a/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
+++ b/java/com/google/gerrit/server/project/SubmitRequirementsAdapter.java
@@ -186,7 +186,12 @@
     }
     ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
     for (Label label : record.labels) {
-      if (skipSubmitRequirementFor(label)) {
+      if (skipSubmitRequirementFor(label)
+          ||
+          // If SubmitRecord is a PASS, then skip all the requirements
+          // that are not a PASS as they would block the overall submit requirement
+          // status from being a PASS
+          (mapStatus(record) == Status.PASS && mapStatus(label) != Status.PASS)) {
         continue;
       }
       String expressionString = String.format("label:%s=%s", label.label, ruleName);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index c7672d1..92da64e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -3024,8 +3024,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body()).doesNotContain("\nPatch Set 2: Code-Review+2\n");
     assertThat(message.body())
         .contains("The change is no longer submittable: Code-Review is unsatisfied now.\n");
@@ -3110,8 +3110,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body()).doesNotContain("\nPatch Set 2: Code-Review+2\n");
     assertThat(message.body()).contains("\nPatch Set 2: Code-Review+1\n");
     assertThat(message.body())
@@ -3198,8 +3198,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body()).contains("\nPatch Set 2: Code-Review-2\n");
     assertThat(message.body())
         .contains("The change is no longer submittable: Code-Review is unsatisfied now.\n");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeNoLongerSubmittableIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeNoLongerSubmittableIT.java
index 1094a42..5bb6dc4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeNoLongerSubmittableIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeNoLongerSubmittableIT.java
@@ -83,8 +83,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body())
         .contains("The change is no longer submittable: Code-Review is unsatisfied now.\n");
     assertThat(message.htmlBody())
@@ -127,8 +127,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body())
         .contains("The change is no longer submittable: Code-Review is unsatisfied now.\n");
     assertThat(message.htmlBody())
@@ -172,8 +172,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body())
         .contains("The change is no longer submittable: Code-Review is unsatisfied now.\n");
     assertThat(message.htmlBody())
@@ -263,8 +263,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body())
         .contains(
             "The change is no longer submittable:"
@@ -315,8 +315,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body()).doesNotContain("The change is no longer submittable");
     assertThat(message.htmlBody()).doesNotContain("The change is no longer submittable");
   }
@@ -361,8 +361,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body()).doesNotContain("The change is no longer submittable");
     assertThat(message.htmlBody()).doesNotContain("The change is no longer submittable");
   }
@@ -398,8 +398,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body()).doesNotContain("The change is no longer submittable");
     assertThat(message.htmlBody()).doesNotContain("The change is no longer submittable");
   }
@@ -435,8 +435,8 @@
             String.format(
                 "Attention is currently required from: %s, %s.\n"
                     + "\n"
-                    + "%s has posted comments on this change.",
-                admin.fullName(), user.fullName(), approver.fullName()));
+                    + "%s has posted comments on this change by %s.",
+                admin.fullName(), user.fullName(), approver.fullName(), admin.fullName()));
     assertThat(message.body()).doesNotContain("The change is no longer submittable");
     assertThat(message.htmlBody()).doesNotContain("The change is no longer submittable");
   }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 77841b2..d618e2f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -450,7 +450,9 @@
 
     Message m = messages.get(0);
     assertThat(m.rcpt()).containsExactly(user.getNameEmail(), observer.getNameEmail());
-    assertThat(m.body()).contains(admin.fullName() + " has posted comments on this change.");
+    assertThat(m.body())
+        .contains(
+            admin.fullName() + " has posted comments on this change by " + admin.fullName() + ".");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
     assertThat(m.body()).contains("Patch Set 1: Code-Review+2");
 
diff --git a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
index 0f4bd2e..e651302 100644
--- a/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
+++ b/javatests/com/google/gerrit/server/project/SubmitRequirementsAdapterTest.java
@@ -337,6 +337,29 @@
   }
 
   @Test
+  public void customSubmitRule_withLabels_withStatusOk() {
+    SubmitRecord submitRecord =
+        createSubmitRecord(
+            "gerrit~PrologRule",
+            Status.OK,
+            Arrays.asList(
+                createLabel("custom-need-label-1", Label.Status.NEED),
+                createLabel("custom-pass-label-2", Label.Status.OK),
+                createLabel("custom-may-label-3", Label.Status.MAY)));
+
+    ImmutableList<SubmitRequirementResult> requirements =
+        SubmitRequirementsAdapter.createResult(submitRecord, labelTypes, psCommitId, false);
+
+    assertThat(requirements).hasSize(1);
+    assertResult(
+        requirements.get(0),
+        /* reqName= */ "custom-pass-label-2",
+        /* submitExpression= */ "label:custom-pass-label-2=gerrit~PrologRule",
+        SubmitRequirementResult.Status.SATISFIED,
+        SubmitRequirementExpressionResult.Status.PASS);
+  }
+
+  @Test
   public void customSubmitRule_withMixOfPassingAndFailingLabels() {
     SubmitRecord submitRecord =
         createSubmitRecord(
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
index f5e403d..650fe41 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.ts
@@ -1265,6 +1265,7 @@
   private getHashtagSuggestions(
     input: string
   ): Promise<AutocompleteSuggestion[]> {
+    const inputReg = input.startsWith('^') ? new RegExp(input) : null;
     return this.restApiService
       .getChangesWithSimilarHashtag(input, throwingErrorCallback)
       .then(response =>
@@ -1272,6 +1273,9 @@
           .flatMap(change => change.hashtags ?? [])
           .filter(isDefined)
           .filter(unique)
+          .filter(hashtag =>
+            inputReg ? inputReg.test(hashtag) : hashtag.includes(input)
+          )
           .map(hashtag => {
             return {name: hashtag, value: hashtag};
           })
diff --git a/resources/com/google/gerrit/server/mail/Comment.soy b/resources/com/google/gerrit/server/mail/Comment.soy
index 4b621b5..8c1840c 100644
--- a/resources/com/google/gerrit/server/mail/Comment.soy
+++ b/resources/com/google/gerrit/server/mail/Comment.soy
@@ -29,7 +29,7 @@
   {@param unsatisfiedSubmitRequirements: ?}
   {@param oldSubmitRequirements: ?}
   {@param newSubmitRequirements: ?}
-  {$fromName} has posted comments on this change.
+  {$fromName} has posted comments on this change by {$change.ownerName}.
   {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {if $unsatisfiedSubmitRequirements}
     {\n}