Merge "Reject uploading project config changes if they contain duplicate SRs"
diff --git a/.bazelversion b/.bazelversion
index 0062ac9..c7cb131 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-5.0.0
+5.3.1
diff --git a/java/com/google/gerrit/index/testing/TestIndexedFields.java b/java/com/google/gerrit/index/testing/TestIndexedFields.java
index f80b8a1..7a120b7 100644
--- a/java/com/google/gerrit/index/testing/TestIndexedFields.java
+++ b/java/com/google/gerrit/index/testing/TestIndexedFields.java
@@ -182,7 +182,10 @@
 
   public static final IndexedField<TestIndexedData, Entities.Change> STORED_PROTO_FIELD =
       IndexedField.<TestIndexedData, Entities.Change>builder(
-              "TestChange", new TypeToken<Entities.Change>() {})
+              "TestChange",
+              new TypeToken<Entities.Change>() {
+                private static final long serialVersionUID = 1L;
+              })
           .stored()
           .build(getter(), setter(), ChangeProtoConverter.INSTANCE);
 
@@ -192,7 +195,10 @@
   public static final IndexedField<TestIndexedData, Iterable<Entities.Change>>
       ITERABLE_STORED_PROTO_FIELD =
           IndexedField.<TestIndexedData, Iterable<Entities.Change>>builder(
-                  "IterableTestChange", new TypeToken<Iterable<Entities.Change>>() {})
+                  "IterableTestChange",
+                  new TypeToken<Iterable<Entities.Change>>() {
+                    private static final long serialVersionUID = 1L;
+                  })
               .stored()
               .build(getter(), setter(), ChangeProtoConverter.INSTANCE);
 
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 7b47248..6471984 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -542,7 +542,7 @@
           }
         }
         ScoreDoc searchAfter = scoreDoc;
-        return new ListResultSet<T>(b.build()) {
+        return new ListResultSet<>(b.build()) {
           @Override
           public Object searchAfter() {
             return searchAfter;
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index e7b2336..1e34b4f 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -414,10 +414,12 @@
         for (int i = 0; i < indexes.size(); i++) {
           ChangeSubIndex subIndex = indexes.get(i);
           searchers[i] = subIndex.acquire();
-          if (opts.searchAfter() != null && opts.searchAfter() instanceof HashMap) {
+          if (opts.searchAfter() != null
+              && opts.searchAfter() instanceof HashMap
+              && ((HashMap<?, ?>) opts.searchAfter()).get(subIndex) instanceof ScoreDoc) {
             hits[i] =
                 searchers[i].searchAfter(
-                    ((HashMap<ChangeSubIndex, ScoreDoc>) opts.searchAfter()).get(subIndex),
+                    (ScoreDoc) ((HashMap<?, ?>) opts.searchAfter()).get(subIndex),
                     query,
                     realPageSize,
                     sort,
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index 0f5629e..cfecd8e 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.GitUpdateFailureException;
@@ -194,7 +193,7 @@
     }
   }
 
-  public void star(Account.Id accountId, Project.NameKey project, Change.Id changeId, Operation op)
+  public void star(Account.Id accountId, Change.Id changeId, Operation op)
       throws IllegalLabelException {
     try (Repository repo = repoManager.openRepository(allUsers)) {
       String refName = RefNames.refsStarredChanges(changeId, accountId);
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
index e718bcb..14aa368 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -45,6 +45,13 @@
     return new AutoValue_AllExternalIds(byKey.build(), byAccount.build(), byEmail.build());
   }
 
+  static AllExternalIds create(
+      ImmutableMap<ExternalId.Key, ExternalId> byKey,
+      ImmutableSetMultimap<Account.Id, ExternalId> byAccount,
+      ImmutableSetMultimap<String, ExternalId> byEmail) {
+    return new AutoValue_AllExternalIds(byKey, byAccount, byEmail);
+  }
+
   public abstract ImmutableMap<ExternalId.Key, ExternalId> byKey();
 
   public abstract ImmutableSetMultimap<Account.Id, ExternalId> byAccount();
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
index 27672bd..1edb284 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheLoader.java
@@ -253,7 +253,7 @@
         }
       }
     }
-    return new AutoValue_AllExternalIds(byKey.build(), byAccount.build(), byEmail.build());
+    return AllExternalIds.create(byKey.build(), byAccount.build(), byEmail.build());
   }
 
   private AllExternalIds reloadAllExternalIds(ObjectId notesRev)
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 3498129..c76c78e 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -595,7 +595,8 @@
 
   private void send(StringBuilder s) {
     String progress = s.toString();
-    logger.atInfo().atMostEvery(1, MINUTES).log(CharMatcher.javaIsoControl().removeFrom(progress));
+    logger.atInfo().atMostEvery(1, MINUTES).log(
+        "%s", CharMatcher.javaIsoControl().removeFrom(progress));
     if (!clientDisconnected) {
       try {
         out.write(Constants.encode(progress));
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 715cb30..f516a0c 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -51,8 +51,8 @@
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.jgit.lib.Config;
 
 /** Delayed execution of tasks using a background thread pool. */
@@ -523,18 +523,23 @@
      * <ol>
      *   <li>{@link #SLEEPING}: if scheduled with a non-zero delay.
      *   <li>{@link #READY}: waiting for an available worker thread.
+     *   <li>{@link #STARTING}: onStart() actively executing on a worker thread.
      *   <li>{@link #RUNNING}: actively executing on a worker thread.
+     *   <li>{@link #STOPPING}: onStop() actively executing on a worker thread.
      *   <li>{@link #DONE}: finished executing, if not periodic.
      * </ol>
      */
     public enum State {
       // Ordered like this so ordinal matches the order we would
       // prefer to see tasks sorted in: done before running,
-      // running before ready, ready before sleeping.
+      // stopping before running, running before starting,
+      // starting before ready, ready before sleeping.
       //
       DONE,
       CANCELLED,
+      STOPPING,
       RUNNING,
+      STARTING,
       READY,
       SLEEPING,
       OTHER
@@ -544,15 +549,16 @@
     private final RunnableScheduledFuture<V> task;
     private final Executor executor;
     private final int taskId;
-    private final AtomicBoolean running;
     private final Instant startTime;
 
+    // runningState is non-null when listener or task code is running in an executor thread
+    private final AtomicReference<State> runningState = new AtomicReference<>();
+
     Task(Runnable runnable, RunnableScheduledFuture<V> task, Executor executor, int taskId) {
       this.runnable = runnable;
       this.task = task;
       this.executor = executor;
       this.taskId = taskId;
-      this.running = new AtomicBoolean();
       this.startTime = Instant.now();
     }
 
@@ -563,10 +569,13 @@
     public State getState() {
       if (isCancelled()) {
         return State.CANCELLED;
+      }
+
+      State r = runningState.get();
+      if (r != null) {
+        return r;
       } else if (isDone() && !isPeriodic()) {
         return State.DONE;
-      } else if (running.get()) {
-        return State.RUNNING;
       }
 
       final long delay = getDelay(TimeUnit.MILLISECONDS);
@@ -587,14 +596,14 @@
     @Override
     public boolean cancel(boolean mayInterruptIfRunning) {
       if (task.cancel(mayInterruptIfRunning)) {
-        // Tiny abuse of running: if the task needs to know it was
-        // canceled (to clean up resources) and it hasn't started
+        // Tiny abuse of runningState: if the task needs to know it
+        // was canceled (to clean up resources) and it hasn't started
         // yet the task's run method won't execute. So we tag it
         // as running and allow it to clean up. This ensures we do
         // not invoke cancel twice.
         //
         if (runnable instanceof CancelableRunnable) {
-          if (running.compareAndSet(false, true)) {
+          if (runningState.compareAndSet(null, State.RUNNING)) {
             ((CancelableRunnable) runnable).cancel();
           } else if (runnable instanceof CanceledWhileRunning) {
             ((CanceledWhileRunning) runnable).setCanceledWhileRunning();
@@ -654,18 +663,21 @@
 
     @Override
     public void run() {
-      if (running.compareAndSet(false, true)) {
+      if (runningState.compareAndSet(null, State.STARTING)) {
         String oldThreadName = Thread.currentThread().getName();
         try {
           executor.onStart(this);
+          runningState.set(State.RUNNING);
           Thread.currentThread().setName(oldThreadName + "[" + task.toString() + "]");
           task.run();
         } finally {
           Thread.currentThread().setName(oldThreadName);
+          runningState.set(State.STOPPING);
           executor.onStop(this);
           if (isPeriodic()) {
-            running.set(false);
+            runningState.set(null);
           } else {
+            runningState.set(State.DONE);
             executor.remove(this);
           }
         }
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 6cdc9ae..a39cbe5 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -112,6 +112,10 @@
     private static ProjectSlice create(Project.NameKey name, int slice, int slices, ScanResult sr) {
       return new AutoValue_AllChangesIndexer_ProjectSlice(name, slice, slices, sr);
     }
+
+    private static ProjectSlice oneSlice(Project.NameKey name, ScanResult sr) {
+      return new AutoValue_AllChangesIndexer_ProjectSlice(name, 0, 1, sr);
+    }
   }
 
   @Override
@@ -178,47 +182,35 @@
   public Callable<Void> reindexProject(
       ChangeIndexer indexer, Project.NameKey project, Task done, Task failed) {
     try (Repository repo = repoManager.openRepository(project)) {
-      return reindexProject(
-          indexer, project, 0, 1, ChangeNotes.Factory.scanChangeIds(repo), done, failed);
+      return reindexProjectSlice(
+          indexer,
+          ProjectSlice.oneSlice(project, ChangeNotes.Factory.scanChangeIds(repo)),
+          done,
+          failed);
     } catch (IOException e) {
       logger.atSevere().log("%s", e.getMessage());
       return null;
     }
   }
 
-  public Callable<Void> reindexProject(
-      ChangeIndexer indexer,
-      Project.NameKey project,
-      int slice,
-      int slices,
-      ScanResult scanResult,
-      Task done,
-      Task failed) {
-    return new ProjectIndexer(indexer, project, slice, slices, scanResult, done, failed);
+  public Callable<Void> reindexProjectSlice(
+      ChangeIndexer indexer, ProjectSlice projectSlice, Task done, Task failed) {
+    return new ProjectSliceIndexer(indexer, projectSlice, done, failed);
   }
 
-  private class ProjectIndexer implements Callable<Void> {
+  private class ProjectSliceIndexer implements Callable<Void> {
     private final ChangeIndexer indexer;
-    private final Project.NameKey project;
-    private final int slice;
-    private final int slices;
-    private final ScanResult scanResult;
+    private final ProjectSlice projectSlice;
     private final ProgressMonitor done;
     private final ProgressMonitor failed;
 
-    private ProjectIndexer(
+    private ProjectSliceIndexer(
         ChangeIndexer indexer,
-        Project.NameKey project,
-        int slice,
-        int slices,
-        ScanResult scanResult,
+        ProjectSlice projectSlice,
         ProgressMonitor done,
         ProgressMonitor failed) {
       this.indexer = indexer;
-      this.project = project;
-      this.slice = slice;
-      this.slices = slices;
-      this.scanResult = scanResult;
+      this.projectSlice = projectSlice;
       this.done = done;
       this.failed = failed;
     }
@@ -232,7 +224,10 @@
       // but the goal is to invalidate that cache as infrequently as we possibly can. And besides,
       // we don't have concrete proof that improving packfile locality would help.
       notesFactory
-          .scan(scanResult, project, id -> (id.get() % slices) == slice)
+          .scan(
+              projectSlice.scanResult(),
+              projectSlice.name(),
+              id -> (id.get() % projectSlice.slices()) == projectSlice.slice())
           .forEach(r -> index(r));
       OnlineReindexMode.end();
       return null;
@@ -271,7 +266,7 @@
 
     @Override
     public String toString() {
-      return "Index all changes of project " + project.get();
+      return "Index project slice " + projectSlice;
     }
   }
 
@@ -351,12 +346,9 @@
               ProjectSlice projectSlice = ProjectSlice.create(name, slice, slices, sr);
               ListenableFuture<?> future =
                   executor.submit(
-                      reindexProject(
+                      reindexProjectSlice(
                           indexerFactory.create(executor, index),
-                          name,
-                          slice,
-                          slices,
-                          projectSlice.scanResult(),
+                          projectSlice,
                           doneTask,
                           failedTask));
               String description = "project " + name + " (" + slice + "/" + slices + ")";
diff --git a/java/com/google/gerrit/server/restapi/account/StarredChanges.java b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
index 12abf3d..173f24b 100644
--- a/java/com/google/gerrit/server/restapi/account/StarredChanges.java
+++ b/java/com/google/gerrit/server/restapi/account/StarredChanges.java
@@ -131,10 +131,7 @@
 
       try {
         starredChangesUtil.star(
-            self.get().getAccountId(),
-            change.getProject(),
-            change.getId(),
-            StarredChangesUtil.Operation.ADD);
+            self.get().getAccountId(), change.getId(), StarredChangesUtil.Operation.ADD);
       } catch (MutuallyExclusiveLabelsException e) {
         throw new ResourceConflictException(e.getMessage());
       } catch (IllegalLabelException e) {
@@ -182,10 +179,7 @@
         throw new AuthException("not allowed remove starred change");
       }
       starredChangesUtil.star(
-          self.get().getAccountId(),
-          rsrc.getChange().getProject(),
-          rsrc.getChange().getId(),
-          StarredChangesUtil.Operation.REMOVE);
+          self.get().getAccountId(), rsrc.getChange().getId(), StarredChangesUtil.Operation.REMOVE);
       return Response.none();
     }
   }
diff --git a/java/com/google/gerrit/server/restapi/config/GetSummary.java b/java/com/google/gerrit/server/restapi/config/GetSummary.java
index d0a1498..23b7a80 100644
--- a/java/com/google/gerrit/server/restapi/config/GetSummary.java
+++ b/java/com/google/gerrit/server/restapi/config/GetSummary.java
@@ -90,14 +90,22 @@
   private TaskSummaryInfo getTaskSummary() {
     Collection<Task<?>> pending = workQueue.getTasks();
     int tasksTotal = pending.size();
+    int tasksStopping = 0;
     int tasksRunning = 0;
+    int tasksStarting = 0;
     int tasksReady = 0;
     int tasksSleeping = 0;
     for (Task<?> task : pending) {
       switch (task.getState()) {
+        case STOPPING:
+          tasksStopping++;
+          break;
         case RUNNING:
           tasksRunning++;
           break;
+        case STARTING:
+          tasksStarting++;
+          break;
         case READY:
           tasksReady++;
           break;
@@ -113,7 +121,9 @@
 
     TaskSummaryInfo taskSummary = new TaskSummaryInfo();
     taskSummary.total = toInteger(tasksTotal);
+    taskSummary.stopping = toInteger(tasksStopping);
     taskSummary.running = toInteger(tasksRunning);
+    taskSummary.starting = toInteger(tasksStarting);
     taskSummary.ready = toInteger(tasksReady);
     taskSummary.sleeping = toInteger(tasksSleeping);
     return taskSummary;
@@ -247,7 +257,9 @@
 
   public static class TaskSummaryInfo {
     public Integer total;
+    public Integer stopping;
     public Integer running;
+    public Integer starting;
     public Integer ready;
     public Integer sleeping;
   }
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 4254e5b..00361ad 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -134,7 +134,9 @@
       switch (task.state) {
         case DONE:
         case CANCELLED:
+        case STARTING:
         case RUNNING:
+        case STOPPING:
         case READY:
           start = format(task.state);
           break;
@@ -204,8 +206,12 @@
         return "....... done";
       case CANCELLED:
         return "..... killed";
+      case STOPPING:
+        return "... stopping";
       case RUNNING:
         return "";
+      case STARTING:
+        return "starting ...";
       case READY:
         return "waiting ....";
       case SLEEPING:
diff --git a/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java b/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java
index c2de4f7..23f2609 100644
--- a/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/util/TaskListenerIT.java
@@ -236,6 +236,29 @@
     assertAwaitQueueSize(--size);
   }
 
+  @Test
+  public void states() throws Exception {
+    executor.execute(runnable);
+    listener.onStart.assertAwait();
+    assertStateIs(Task.State.STARTING);
+
+    listener.onStart.complete();
+    runnable.run.assertAwait();
+    assertStateIs(Task.State.RUNNING);
+
+    runnable.run.complete();
+    listener.onStop.assertAwait();
+    assertStateIs(Task.State.STOPPING);
+
+    listener.onStop.complete();
+    assertAwaitQueueIsEmpty();
+    assertStateIs(Task.State.DONE);
+  }
+
+  private void assertStateIs(Task.State state) {
+    assertThat(forwarder.task.getState()).isEqualTo(state);
+  }
+
   private int assertQueueBlockedOnExecution(Runnable runnable) {
     int expectedSize = workQueue.getTasks().size() + 1;
     executor.execute(runnable);
@@ -247,6 +270,10 @@
     assertThat(workQueue.getTasks().size()).isEqualTo(size);
   }
 
+  private void assertAwaitQueueIsEmpty() throws InterruptedException {
+    assertAwaitQueueSize(0);
+  }
+
   /** Fails if the waiting time elapses before the count is reached, otherwise passes */
   private void assertAwaitQueueSize(int size) throws InterruptedException {
     long i = 0;
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
index 01ae960..af42afb 100644
--- a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -55,7 +55,6 @@
 
   @Test
   @UseClockStep
-  @SuppressWarnings("unchecked")
   public void stopQueryIfNoMoreResults() throws Exception {
     // create 2 visible changes
     TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
@@ -72,7 +71,8 @@
         .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
         .update();
 
-    AbstractFakeIndex idx = (AbstractFakeIndex) changeIndexCollection.getSearchIndex();
+    AbstractFakeIndex<?, ?, ?> idx =
+        (AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
     newQuery("status:new").withLimit(5).get();
     // Since the limit of the query (i.e. 5) is more than the total number of changes (i.e. 4),
     // only 1 index search is expected.
@@ -81,7 +81,6 @@
 
   @Test
   @UseClockStep
-  @SuppressWarnings("unchecked")
   public void noLimitQueryPaginates() throws Exception {
     TestRepository<InMemoryRepositoryManager.Repo> testRepo = createProject("repo");
     // create 4 changes
@@ -97,7 +96,8 @@
         .add(allowCapability(QUERY_LIMIT).group(REGISTERED_USERS).range(0, 2))
         .update();
 
-    AbstractFakeIndex idx = (AbstractFakeIndex) changeIndexCollection.getSearchIndex();
+    AbstractFakeIndex<?, ?, ?> idx =
+        (AbstractFakeIndex<?, ?, ?>) changeIndexCollection.getSearchIndex();
 
     // 2 index searches are expected. The first index search will run with size 2 (i.e.
     // the configured query-limit), and then we will paginate to get the remaining 2
diff --git a/plugins/gitiles b/plugins/gitiles
index 648b1df..24529d2 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 648b1df92b887ed5011e3a2fbf18fd2b9e8622b3
+Subproject commit 24529d232268ac51fd6850770f70dc0fcd732dd8
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index e012bd1..ec05cad 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -10,9 +10,9 @@
     "@babel/highlight" "^7.18.6"
 
 "@babel/helper-validator-identifier@^7.18.6":
-  version "7.18.6"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
-  integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
+  version "7.19.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
+  integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
 
 "@babel/highlight@^7.18.6":
   version "7.18.6"
@@ -35,16 +35,11 @@
   resolved "https://registry.yarnpkg.com/@gerritcodereview/typescript-api/-/typescript-api-3.7.0.tgz#ae3886b5c4ddc6a02659a11d577e1df0b6158727"
   integrity sha512-8zeZClN1gur+Isrn02bRXJ0wUjYvK99jQxg36ZbDelrGDglXKddf8QQkZmaH9sYIRcCFDLlh5+ZlRUTcXTuDVA==
 
-"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.4.0":
+"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.4.0":
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.4.1.tgz#3f587eec5708692135bc9e94cf396130604979f3"
   integrity sha512-qDv4851VFSaBWzpS02cXHclo40jsbAjRXnebNXpm0uVg32kCneZPo9RYVQtrTNICtZ+1wAYHu1ZtxWSWMbKrBw==
 
-"@lit/reactive-element@^1.3.0":
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.3.2.tgz#43e470537b6ec2c23510c07812616d5aa27a17cd"
-  integrity sha512-A2e18XzPMrIh35nhIdE4uoqRzoIpEU5vZYuQN4S3Ee1zkGdYC27DP12pewbw/RLgPHzaE4kx/YqxMzebOpm0dA==
-
 "@nodelib/fs.scandir@2.1.5":
   version "2.1.5"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -131,9 +126,9 @@
     "@polymer/polymer" "^3.0.5"
 
 "@polymer/polymer@^3.0.5", "@polymer/polymer@^3.4.1":
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.4.1.tgz#333bef25711f8411bb5624fb3eba8212ef8bee96"
-  integrity sha512-KPWnhDZibtqKrUz7enIPOiO4ZQoJNOuLwqrhV2MXzIt3VVnUVJVG5ORz4Z2sgO+UZ+/UZnPD0jqY+jmw/+a9mQ==
+  version "3.5.1"
+  resolved "https://registry.yarnpkg.com/@polymer/polymer/-/polymer-3.5.1.tgz#4b5234e43b8876441022bcb91313ab3c4a29f0c8"
+  integrity sha512-JlAHuy+1qIC6hL1ojEUfIVD58fzTpJAoCxFwV5yr0mYTXV1H8bz5zy0+rC963Cgr9iNXQ4T9ncSjC2fkF9BQfw==
   dependencies:
     "@webcomponents/shadycss" "^1.9.1"
 
@@ -289,9 +284,9 @@
   integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
 
 "@types/node@*":
-  version "18.7.18"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.18.tgz#633184f55c322e4fb08612307c274ee6d5ed3154"
-  integrity sha512-m+6nTEOadJZuTPkKR/SYK3A2d7FZrgElol9UP1Kae90VVU4a6mxnPuLiIW1m4Cq4gZ/nWb9GrdVXJCoCazDAbg==
+  version "18.8.3"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c"
+  integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==
 
 "@types/parse5@^6.0.1":
   version "6.0.3"
@@ -1070,44 +1065,28 @@
     vary "^1.1.2"
 
 lit-element@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.2.0.tgz#9c981c55dfd9a8f124dc863edb62cc529d434db7"
-  integrity sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==
+  version "3.2.2"
+  resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.2.2.tgz#d148ab6bf4c53a33f707a5168e087725499e5f2b"
+  integrity sha512-6ZgxBR9KNroqKb6+htkyBwD90XGRiqKDHVrW/Eh0EZ+l+iC+u+v+w3/BA5NGi4nizAVHGYvQBHUDuSmLjPp7NQ==
   dependencies:
     "@lit/reactive-element" "^1.3.0"
     lit-html "^2.2.0"
 
-lit-html@^2.0.0, lit-html@^2.3.0:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.3.1.tgz#56f15104ea75c0a702904893e3409d0e89e2a2b9"
-  integrity sha512-FyKH6LTW6aBdkfNhNSHyZTnLgJSTe5hMk7HFtc/+DcN1w74C215q8B+Cfxc2OuIEpBNcEKxgF64qL8as30FDHA==
+lit-html@^2.0.0, lit-html@^2.2.0, lit-html@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.4.0.tgz#b510430f39a56ec959167ed1187241a4e3ab1574"
+  integrity sha512-G6qXu4JNUpY6aaF2VMfaszhO9hlWw0hOTRFDmuMheg/nDYGB+2RztUSOyrzALAbr8Nh0Y7qjhYkReh3rPnplVg==
   dependencies:
     "@types/trusted-types" "^2.0.2"
 
-lit-html@^2.2.0:
-  version "2.2.6"
-  resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.2.6.tgz#e70679605420a34c4f3cbd0c483b2fb1fff781df"
-  integrity sha512-xOKsPmq/RAKJ6dUeOxhmOYFjcjf0Q7aSdfBJgdJkOfCUnkmmJPxNrlZpRBeVe1Gg50oYWMlgm6ccAE/SpJgSdw==
-  dependencies:
-    "@types/trusted-types" "^2.0.2"
-
-lit@^2.0.0:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/lit/-/lit-2.3.1.tgz#2cf1c2042da1e44c7a7cc72dff2d72303fd26f48"
-  integrity sha512-TejktDR4mqG3qB32Y8Lm5Lye3c8SUehqz7qRsxe1PqGYL6me2Ef+jeQAEqh20BnnGncv4Yxy2njEIT0kzK1WCw==
+lit@^2.0.0, lit@^2.2.3:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/lit/-/lit-2.4.0.tgz#e33a0f463e17408f6e7f71464e6a266e60a5bb77"
+  integrity sha512-fdgzxEtLrZFQU/BqTtxFQCLwlZd9bdat+ltzSFjvWkZrs7eBmeX0L5MHUMb3kYIkuS8Xlfnii/iI5klirF8/Xg==
   dependencies:
     "@lit/reactive-element" "^1.4.0"
     lit-element "^3.2.0"
-    lit-html "^2.3.0"
-
-lit@^2.2.3:
-  version "2.2.6"
-  resolved "https://registry.yarnpkg.com/lit/-/lit-2.2.6.tgz#4ef223e88517c000b0c01baf2e3535e61a75a5b5"
-  integrity sha512-K2vkeGABfSJSfkhqHy86ujchJs3NR9nW1bEEiV+bXDkbiQ60Tv5GUausYN2mXigZn8lC1qXuc46ArQRKYmumZw==
-  dependencies:
-    "@lit/reactive-element" "^1.3.0"
-    lit-element "^3.2.0"
-    lit-html "^2.2.0"
+    lit-html "^2.4.0"
 
 log-update@^4.0.0:
   version "4.0.0"
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index 7fe070c..66ad38c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -30,7 +30,6 @@
 import {ReviewInput} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
 import {assertIsDefined} from '../../../utils/common-util';
-import {CURRENT} from '../../../utils/patch-set-util';
 import {fireReload} from '../../../utils/event-util';
 import {submitRequirementsStyles} from '../../../styles/gr-submit-requirements-styles';
 import {
@@ -321,7 +320,11 @@
       },
     };
     return this.restApiService
-      .saveChangeReview(this.change._number, CURRENT, review)
+      .saveChangeReview(
+        this.change._number,
+        this.change.current_revision,
+        review
+      )
       .then(() => {
         fireReload(this, true);
       });
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
index 2117ec5..4e78e8a 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
@@ -18,7 +18,7 @@
   createSubmitRequirementResultInfo,
 } from '../../../test/test-data-generators';
 import {ParsedChangeInfo} from '../../../types/types';
-import {query, queryAndAssert} from '../../../test/test-utils';
+import {query, queryAndAssert, stubRestApi} from '../../../test/test-utils';
 import {GrButton} from '../../shared/gr-button/gr-button';
 import {ChangeStatus, SubmitRequirementResultInfo} from '../../../api/rest-api';
 
@@ -321,6 +321,22 @@
       assert.isUndefined(query(element, '.quickApprove'));
     });
 
+    test('uses patchset from change', async () => {
+      const saveChangeReview = stubRestApi('saveChangeReview').resolves();
+      const element = await fixture<GrSubmitRequirementHovercard>(
+        html`<gr-submit-requirement-hovercard
+          .requirement=${submitRequirement}
+          .change=${change}
+          .account=${account}
+        ></gr-submit-requirement-hovercard>`
+      );
+
+      queryAndAssert<GrButton>(element, '.quickApprove > gr-button').click();
+
+      assert.equal(saveChangeReview.callCount, 1);
+      assert.equal(saveChangeReview.firstCall.args[1], change.current_revision);
+    });
+
     test('override button renders', async () => {
       const submitRequirement: SubmitRequirementResultInfo = {
         ...createSubmitRequirementResultInfo(),
diff --git a/polygerrit-ui/app/models/checks/checks-model.ts b/polygerrit-ui/app/models/checks/checks-model.ts
index 2b92529..db208d5 100644
--- a/polygerrit-ui/app/models/checks/checks-model.ts
+++ b/polygerrit-ui/app/models/checks/checks-model.ts
@@ -151,7 +151,12 @@
   };
 }
 
-const FETCH_RESULT_TIMEOUT_MS = 10000;
+/**
+ * Android's Checks Plugin has a 15s timeout internally. So we are using
+ * something slightly larger, so that we get a proper error from the plugin,
+ * if they run into timeout issues.
+ */
+const FETCH_RESULT_TIMEOUT_MS = 16000;
 
 /**
  * Can be used in `reduce()` to collect all results from all runs from all
@@ -185,9 +190,11 @@
 
   private checkToPluginMap = new Map<string, string>();
 
-  private changeNum?: NumericChangeId;
+  // visible for testing
+  changeNum?: NumericChangeId;
 
-  private latestPatchNum?: PatchSetNumber;
+  // visible for testing
+  latestPatchNum?: PatchSetNumber;
 
   private readonly documentVisibilityChange$ = new BehaviorSubject(undefined);
 
@@ -384,6 +391,9 @@
     this.reporting.time(Timing.CHECKS_LOAD);
     this.subscriptions = [
       this.changeModel.changeNum$.subscribe(x => (this.changeNum = x)),
+      this.changeModel.latestPatchNum$.subscribe(
+        x => (this.latestPatchNum = x)
+      ),
       this.pluginsModel.checksPlugins$.subscribe(plugins => {
         for (const plugin of plugins) {
           this.register(plugin);
@@ -750,8 +760,10 @@
         patchset === ChecksPatchset.LATEST
           ? this.changeModel.latestPatchNum$
           : this.checksSelectedPatchsetNumber$,
-        this.reloadSubjects[pluginName].pipe(throttleTime(1000)),
-        timer(0, pollIntervalMs),
+        this.reloadSubjects[pluginName].pipe(
+          throttleTime(1000, undefined, {trailing: true, leading: true})
+        ),
+        pollIntervalMs === 0 ? from([0]) : timer(0, pollIntervalMs),
         this.documentVisibilityChange$,
       ])
         .pipe(
diff --git a/polygerrit-ui/app/models/checks/checks-model_test.ts b/polygerrit-ui/app/models/checks/checks-model_test.ts
index 83ed464..3489c5a 100644
--- a/polygerrit-ui/app/models/checks/checks-model_test.ts
+++ b/polygerrit-ui/app/models/checks/checks-model_test.ts
@@ -7,6 +7,7 @@
 import './checks-model';
 import {ChecksModel, ChecksPatchset, ChecksProviderState} from './checks-model';
 import {
+  Action,
   Category,
   CheckRun,
   ChecksApiConfig,
@@ -22,6 +23,7 @@
 import {assert} from '@open-wc/testing';
 import {testResolver} from '../../test/common-test-setup';
 import {changeViewModelToken} from '../views/change';
+import {NumericChangeId, PatchSetNumber} from '../../api/rest-api';
 
 const PLUGIN_NAME = 'test-plugin';
 
@@ -42,8 +44,12 @@
   },
 ];
 
-const CONFIG: ChecksApiConfig = {
-  fetchPollingIntervalSeconds: 1000,
+const CONFIG_POLLING_5S: ChecksApiConfig = {
+  fetchPollingIntervalSeconds: 5,
+};
+
+const CONFIG_POLLING_NONE: ChecksApiConfig = {
+  fetchPollingIntervalSeconds: 0,
 };
 
 function createProvider(): ChecksProvider {
@@ -82,13 +88,67 @@
     const provider = createProvider();
     const fetchSpy = sinon.spy(provider, 'fetch');
 
-    model.register({pluginName: 'test-plugin', provider, config: CONFIG});
+    model.register({
+      pluginName: 'test-plugin',
+      provider,
+      config: CONFIG_POLLING_NONE,
+    });
     await waitUntil(() => change === undefined);
 
     const testChange = createParsedChange();
     model.changeModel.updateStateChange(testChange);
     await waitUntil(() => change === testChange);
     await waitUntilCalled(fetchSpy, 'fetch');
+
+    assert.equal(
+      model.latestPatchNum,
+      testChange.revisions[testChange.current_revision]
+        ._number as PatchSetNumber
+    );
+    assert.equal(model.changeNum, testChange._number);
+  });
+
+  test('reload throttle', async () => {
+    const clock = sinon.useFakeTimers();
+    let change: ParsedChangeInfo | undefined = undefined;
+    model.changeModel.change$.subscribe(c => (change = c));
+    const provider = createProvider();
+    const fetchSpy = sinon.spy(provider, 'fetch');
+
+    model.register({
+      pluginName: 'test-plugin',
+      provider,
+      config: CONFIG_POLLING_NONE,
+    });
+    await waitUntil(() => change === undefined);
+
+    const testChange = createParsedChange();
+    model.changeModel.updateStateChange(testChange);
+    await waitUntil(() => change === testChange);
+    clock.tick(1);
+    assert.equal(fetchSpy.callCount, 1);
+
+    // The second reload call will be processed, but only after a 1s throttle.
+    model.reload('test-plugin');
+    clock.tick(100);
+    assert.equal(fetchSpy.callCount, 1);
+    // 2000 ms is greater than the 1000 ms throttle time.
+    clock.tick(2000);
+    assert.equal(fetchSpy.callCount, 2);
+  });
+
+  test('triggerAction', async () => {
+    model.changeNum = 314 as NumericChangeId;
+    model.latestPatchNum = 13 as PatchSetNumber;
+    const action: Action = {
+      name: 'test action',
+      callback: () => undefined,
+    };
+    const spy = sinon.spy(action, 'callback');
+    model.triggerAction(action, undefined, 'none');
+    assert.isTrue(spy.calledOnce);
+    assert.equal(spy.lastCall.args[0], 314);
+    assert.equal(spy.lastCall.args[1], 13);
   });
 
   test('model.updateStateSetProvider', () => {
@@ -204,4 +264,58 @@
     assert.lengthOf(current.runs[0].results!, 1);
     assert.equal(current.runs[0].results![0].summary, 'new');
   });
+
+  test('polls for changes', async () => {
+    const clock = sinon.useFakeTimers();
+    let change: ParsedChangeInfo | undefined = undefined;
+    model.changeModel.change$.subscribe(c => (change = c));
+    const provider = createProvider();
+    const fetchSpy = sinon.spy(provider, 'fetch');
+
+    model.register({
+      pluginName: 'test-plugin',
+      provider,
+      config: CONFIG_POLLING_5S,
+    });
+    await waitUntil(() => change === undefined);
+    clock.tick(1);
+    const testChange = createParsedChange();
+    model.changeModel.updateStateChange(testChange);
+    await waitUntil(() => change === testChange);
+    await waitUntilCalled(fetchSpy, 'fetch');
+    clock.tick(1);
+    const pollCount = fetchSpy.callCount;
+
+    // polling should continue while we wait
+    clock.tick(CONFIG_POLLING_5S.fetchPollingIntervalSeconds * 1000 * 2);
+
+    assert.isTrue(fetchSpy.callCount > pollCount);
+  });
+
+  test('does not poll when config specifies 0 seconds', async () => {
+    const clock = sinon.useFakeTimers();
+    let change: ParsedChangeInfo | undefined = undefined;
+    model.changeModel.change$.subscribe(c => (change = c));
+    const provider = createProvider();
+    const fetchSpy = sinon.spy(provider, 'fetch');
+
+    model.register({
+      pluginName: 'test-plugin',
+      provider,
+      config: CONFIG_POLLING_NONE,
+    });
+    await waitUntil(() => change === undefined);
+    clock.tick(1);
+    const testChange = createParsedChange();
+    model.changeModel.updateStateChange(testChange);
+    await waitUntil(() => change === testChange);
+    await waitUntilCalled(fetchSpy, 'fetch');
+    clock.tick(1);
+    const pollCount = fetchSpy.callCount;
+
+    // polling should not happen
+    clock.tick(60 * 1000);
+
+    assert.equal(fetchSpy.callCount, pollCount);
+  });
 });
diff --git a/polygerrit-ui/app/models/views/change.ts b/polygerrit-ui/app/models/views/change.ts
index 826ec66..a2d3506 100644
--- a/polygerrit-ui/app/models/views/change.ts
+++ b/polygerrit-ui/app/models/views/change.ts
@@ -155,11 +155,18 @@
     super(undefined);
     this.state$.subscribe(s => {
       if (s?.usp || s?.forceReload || s?.openReplyDialog) {
-        this.updateState({
-          usp: undefined,
-          forceReload: undefined,
-          openReplyDialog: undefined,
-        });
+        // We had been running into issues with directly calling
+        // `this.updateState()` without `setTimeout()`, because we want to
+        // first allow all subscribers to process the first state update before
+        // creating another one. For some unknown reason some subscribers even
+        // got the state updates out of order.
+        setTimeout(() => {
+          this.updateState({
+            usp: undefined,
+            forceReload: undefined,
+            openReplyDialog: undefined,
+          });
+        }, 0);
       }
     });
   }
diff --git a/polygerrit-ui/app/styles/themes/app-theme.ts b/polygerrit-ui/app/styles/themes/app-theme.ts
index 117e7cc..e148da9 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.ts
+++ b/polygerrit-ui/app/styles/themes/app-theme.ts
@@ -99,6 +99,8 @@
     --purple-100: #e9d2fd;
     --purple-50: #f3e8fd;
     --purple-tonal: #523272;
+    --deep-purple-800: #4527a0;
+    --deep-purple-600: #5e35b1;
     --pink-800: #b80672;
     --pink-500: #f538a0;
     --pink-50: #fde7f3;
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.ts b/polygerrit-ui/app/styles/themes/dark-theme.ts
index d11e372..2330041 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.ts
+++ b/polygerrit-ui/app/styles/themes/dark-theme.ts
@@ -219,8 +219,8 @@
     --dark-remove-highlight-color: #62110f;
     --light-remove-highlight-color: #320404;
 
-    --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
-    --light-rebased-add-highlight-color: #487165;
+    --dark-rebased-add-highlight-color: var(--deep-purple-800);
+    --light-rebased-add-highlight-color: var(--deep-purple-600);
     --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
     --light-rebased-remove-highlight-color: #2f3f2f;
 
diff --git a/tools/BUILD b/tools/BUILD
index 8d6d48f..c06cfdb 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -44,10 +44,11 @@
         "-XepDisableWarningsInGeneratedCode",
         # The XepDisableWarningsInGeneratedCode disables only warnings, but
         # not errors. We should manually exclude all files generated by
-        # AutoValue; such files always start $AutoValue_.....
+        # AutoValue; such files always start AutoValue_..., $AutoValue_...
+        # or $$AutoValue_...
         # XepExcludedPaths is a regexp. If you need more paths - use | as
         # separator.
-        "-XepExcludedPaths:.*/\\\\$$AutoValue_.*\\.java",
+        "-XepExcludedPaths:.*/\\\\$$?\\\\$$?AutoValue_.*\\.java",
         "-Xep:AlmostJavadoc:ERROR",
         "-Xep:AlwaysThrows:ERROR",
         "-Xep:AmbiguousMethodReference:ERROR",
@@ -66,7 +67,7 @@
         "-Xep:AutoValueConstructorOrderChecker:ERROR",
         "-Xep:AutoValueFinalMethods:ERROR",
         "-Xep:AutoValueImmutableFields:ERROR",
-        # "-Xep:AutoValueSubclassLeaked:WARN",
+        "-Xep:AutoValueSubclassLeaked:ERROR",
         "-Xep:BadAnnotationImplementation:ERROR",
         "-Xep:BadComparable:ERROR",
         "-Xep:BadImport:ERROR",
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 37d8b9c..d6f2ef63 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -135,8 +135,8 @@
 
     maven_jar(
         name = "error-prone-annotations",
-        artifact = "com.google.errorprone:error_prone_annotations:2.10.0",
-        sha1 = "9bc20b94d3ac42489cf6ce1e42509c86f6f861a1",
+        artifact = "com.google.errorprone:error_prone_annotations:2.15.0",
+        sha1 = "38c8485a652f808c8c149150da4e5c2b0bd17f9a",
     )
 
     FLOGGER_VERS = "0.7.4"