Merge "access-control.txt: Fix ref on which Forge Committer must be assigned"
diff --git a/Documentation/dev-processes.txt b/Documentation/dev-processes.txt
index 409038c..8014977 100644
--- a/Documentation/dev-processes.txt
+++ b/Documentation/dev-processes.txt
@@ -56,7 +56,7 @@
 a year in June. Non-Google link:dev-roles.html#maintainer[maintainers]
 can nominate themselves by posting an informal application on the
 non-public mailto:gerritcodereview-community-managers@googlegroups.com[
-community manager mailing list] when the call for nomiations is sent to
+community manager mailing list] when the call for nominations is sent to
 the maintainers list by a community manager.
 
 The list with all candidates will be published at the beginning of the
diff --git a/java/Main.java b/java/Main.java
index e824a95..c04db2c 100644
--- a/java/Main.java
+++ b/java/Main.java
@@ -16,7 +16,7 @@
 public final class Main {
   private static final String FLOGGER_BACKEND_PROPERTY = "flogger.backend_factory";
   private static final String FLOGGER_LOGGING_CONTEXT = "flogger.logging_context";
-  private static final Runtime.Version MIN_JAVA_VERSION = Runtime.Version.parse("17.0.5");
+  private static final Runtime.Version MIN_JAVA_VERSION = Runtime.Version.parse("11.0.10");
 
   // We don't do any real work here because we need to import
   // the archive lookup code and we cannot import a class in
diff --git a/java/com/google/gerrit/index/query/AndSource.java b/java/com/google/gerrit/index/query/AndSource.java
index 3adf881..6de0712 100644
--- a/java/com/google/gerrit/index/query/AndSource.java
+++ b/java/com/google/gerrit/index/query/AndSource.java
@@ -17,11 +17,12 @@
 import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.PaginationType;
 import java.util.Collection;
 import java.util.List;
 
 public class AndSource<T> extends AndPredicate<T> implements DataSource<T> {
-  protected final DataSource<T> source;
+  protected final FilteredSource<T> filteredSource;
 
   private final int start;
   private final int cardinality;
@@ -58,18 +59,18 @@
     if (selectedSource == null) {
       throw new IllegalArgumentException("No DataSource Found");
     }
-    this.source = toPaginatingSource(selectedSource);
+    this.filteredSource = toDataSource(selectedSource);
     this.cardinality = c;
   }
 
   @Override
   public ResultSet<T> read() {
-    return source.read();
+    return filteredSource.read();
   }
 
   @Override
   public ResultSet<FieldBundle> readRaw() {
-    return source.readRaw();
+    return filteredSource.readRaw();
   }
 
   @Override
@@ -91,17 +92,44 @@
   }
 
   @SuppressWarnings("unchecked")
-  private PaginatingSource<T> toPaginatingSource(Predicate<T> pred) {
-    return new PaginatingSource<>((DataSource<T>) pred, start, indexConfig) {
-      @Override
-      protected boolean match(T object) {
-        return AndSource.this.match(object);
-      }
+  private FilteredSource<T> toDataSource(Predicate<T> pred) {
+    if (indexConfig.paginationType().equals(PaginationType.NONE)) {
+      return new DatasourceWithoutPagination((DataSource<T>) pred, start, indexConfig);
+    }
+    return new DatasourceWithPagination((DataSource<T>) pred, start, indexConfig);
+  }
 
-      @Override
-      protected boolean isMatchable() {
-        return AndSource.this.isMatchable();
-      }
-    };
+  private class DatasourceWithoutPagination extends FilteredSource<T> {
+
+    public DatasourceWithoutPagination(DataSource<T> source, int start, IndexConfig indexConfig) {
+      super(source, start, indexConfig);
+    }
+
+    @Override
+    protected boolean match(T object) {
+      return AndSource.this.match(object);
+    }
+
+    @Override
+    protected boolean isMatchable() {
+      return AndSource.this.isMatchable();
+    }
+  }
+
+  private class DatasourceWithPagination extends PaginatingSource<T> {
+
+    public DatasourceWithPagination(DataSource<T> source, int start, IndexConfig indexConfig) {
+      super(source, start, indexConfig);
+    }
+
+    @Override
+    protected boolean match(T object) {
+      return AndSource.this.match(object);
+    }
+
+    @Override
+    protected boolean isMatchable() {
+      return AndSource.this.isMatchable();
+    }
   }
 }
diff --git a/java/com/google/gerrit/index/query/FilteredSource.java b/java/com/google/gerrit/index/query/FilteredSource.java
new file mode 100644
index 0000000..fb31eb6
--- /dev/null
+++ b/java/com/google/gerrit/index/query/FilteredSource.java
@@ -0,0 +1,94 @@
+// 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.index.query;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.IndexConfig;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FilteredSource<T> implements DataSource<T> {
+
+  protected final DataSource<T> source;
+  protected final int start;
+  protected final int cardinality;
+  protected final IndexConfig indexConfig;
+  private static final int PARTITION_SIZE = 50;
+
+  public FilteredSource(DataSource<T> source, int start, IndexConfig indexConfig) {
+    checkArgument(start >= 0, "negative start: %s", start);
+    this.source = source;
+    this.start = start;
+    this.cardinality = source.getCardinality();
+    this.indexConfig = indexConfig;
+  }
+
+  @Override
+  public ResultSet<T> read() {
+    if (source == null) {
+      throw new StorageException("No DataSource defined.");
+    }
+    // ResultSets are lazy. Calling #read here first and then dealing with ResultSets only when
+    // requested allows the index to run asynchronous queries.
+    ResultSet<T> resultSet = source.read();
+    return new LazyResultSet<>(
+        () -> {
+          List<T> r = new ArrayList<>();
+          for (T data : buffer(resultSet)) {
+            if (!isMatchable() || match(data)) {
+              r.add(data);
+            }
+          }
+          if (start >= r.size()) {
+            return ImmutableList.of();
+          } else if (start > 0) {
+            return ImmutableList.copyOf(r.subList(start, r.size()));
+          }
+          return ImmutableList.copyOf(r);
+        });
+  }
+
+  @Override
+  public ResultSet<FieldBundle> readRaw() {
+    return source.readRaw();
+  }
+
+  protected Iterable<T> buffer(ResultSet<T> scanner) {
+    return FluentIterable.from(Iterables.partition(scanner, PARTITION_SIZE))
+        .transformAndConcat(this::transformBuffer);
+  }
+
+  protected List<T> transformBuffer(List<T> buffer) {
+    return buffer;
+  }
+
+  @Override
+  public int getCardinality() {
+    return cardinality;
+  }
+
+  protected boolean match(T object) {
+    return true;
+  }
+
+  protected boolean isMatchable() {
+    return true;
+  }
+}
diff --git a/java/com/google/gerrit/index/query/PaginatingSource.java b/java/com/google/gerrit/index/query/PaginatingSource.java
index 98a0ed3..be789ee 100644
--- a/java/com/google/gerrit/index/query/PaginatingSource.java
+++ b/java/com/google/gerrit/index/query/PaginatingSource.java
@@ -14,11 +14,7 @@
 
 package com.google.gerrit.index.query;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.index.IndexConfig;
@@ -27,18 +23,10 @@
 import java.util.ArrayList;
 import java.util.List;
 
-public class PaginatingSource<T> implements DataSource<T> {
-  protected final DataSource<T> source;
-  private final int start;
-  private final int cardinality;
-  private final IndexConfig indexConfig;
+public class PaginatingSource<T> extends FilteredSource<T> {
 
   public PaginatingSource(DataSource<T> source, int start, IndexConfig indexConfig) {
-    checkArgument(start >= 0, "negative start: %s", start);
-    this.source = source;
-    this.start = start;
-    this.cardinality = source.getCardinality();
-    this.indexConfig = indexConfig;
+    super(source, start, indexConfig);
   }
 
   @Override
@@ -63,13 +51,7 @@
             pageResultSize++;
           }
 
-          if (last != null
-              && source instanceof Paginated
-              // TODO: this fix is only for the stable branches and the real refactoring would be to
-              // restore the logic
-              // for the filtering in AndSource (L58 - 64) as per
-              // https://gerrit-review.googlesource.com/c/gerrit/+/345634/9
-              && !indexConfig.paginationType().equals(PaginationType.NONE)) {
+          if (last != null && source instanceof Paginated) {
             // Restart source and continue if we have not filled the
             // full limit the caller wants.
             //
@@ -117,34 +99,6 @@
     throw new UnsupportedOperationException("not implemented");
   }
 
-  private Iterable<T> buffer(ResultSet<T> scanner) {
-    return FluentIterable.from(Iterables.partition(scanner, 50))
-        .transformAndConcat(this::transformBuffer);
-  }
-
-  /**
-   * Checks whether the given object matches.
-   *
-   * @param object the object to be matched
-   * @return whether the given object matches
-   */
-  protected boolean match(T object) {
-    return true;
-  }
-
-  protected boolean isMatchable() {
-    return true;
-  }
-
-  protected List<T> transformBuffer(List<T> buffer) {
-    return buffer;
-  }
-
-  @Override
-  public int getCardinality() {
-    return cardinality;
-  }
-
   private int getNextPageSize(int pageSize, int pageSizeMultiplier) {
     List<Integer> possiblePageSizes = new ArrayList<>(3);
     try {
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index d847a06..f49cecb 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.index.IndexCollection;
 import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.index.IndexRewriter;
+import com.google.gerrit.index.PaginationType;
 import com.google.gerrit.index.QueryOptions;
 import com.google.gerrit.index.SchemaDefinitions;
 import com.google.gerrit.metrics.Description;
@@ -296,7 +297,9 @@
 
         @SuppressWarnings("unchecked")
         DataSource<T> s = (DataSource<T>) pred;
-        if (initialPageSize < limit && !(pred instanceof AndSource)) {
+        if (!indexConfig.paginationType().equals(PaginationType.NONE)
+            && initialPageSize < limit
+            && !(pred instanceof AndSource)) {
           s = new PaginatingSource<>(s, start, indexConfig);
         }
         sources.add(s);
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 41e1910..b940ccc 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -279,7 +279,7 @@
     } finally {
       listLock.unlock();
     }
-    indexer.get().index(newProjectName);
+    evictAndReindex(newProjectName);
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/AndChangeSource.java b/java/com/google/gerrit/server/query/change/AndChangeSource.java
index e4f768e..6c03425 100644
--- a/java/com/google/gerrit/server/query/change/AndChangeSource.java
+++ b/java/com/google/gerrit/server/query/change/AndChangeSource.java
@@ -33,7 +33,8 @@
 
   @Override
   public boolean hasChange() {
-    return source instanceof ChangeDataSource && ((ChangeDataSource) source).hasChange();
+    return filteredSource instanceof ChangeDataSource
+        && ((ChangeDataSource) filteredSource).hasChange();
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index e46f9e4..b0e58c5 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.restapi.change;
 
 import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -71,7 +72,7 @@
     ReviewerModification modification =
         reviewerModifier.prepare(rsrc.getNotes(), rsrc.getUser(), input, true);
     if (modification.op == null) {
-      return Response.ok(modification.result);
+      return Response.withStatusCode(SC_BAD_REQUEST, modification.result);
     }
     try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
       try (BatchUpdate bu =
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 80c220a..979529e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -105,14 +105,14 @@
     // Attempt to add overly large group as reviewers.
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
-    ReviewerResult result = addReviewer(changeId, largeGroup);
+    ReviewerResult result = addReviewer(changeId, largeGroup, SC_BAD_REQUEST);
     assertThat(result.input).isEqualTo(largeGroup);
     assertThat(result.confirm).isNull();
     assertThat(result.error).contains("has too many members to add them all as reviewers");
     assertThat(result.reviewers).isNull();
 
     // Attempt to add medium group without confirmation.
-    result = addReviewer(changeId, mediumGroup);
+    result = addReviewer(changeId, mediumGroup, SC_BAD_REQUEST);
     assertThat(result.input).isEqualTo(mediumGroup);
     assertThat(result.confirm).isTrue();
     assertThat(result.error)
@@ -162,6 +162,48 @@
   }
 
   @Test
+  public void addCcEmailWithoutAccount() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String testEmailAddress = "email@without.account";
+
+    // Add a reviewer
+    ReviewerInput ri = new ReviewerInput();
+    ri.reviewer = user.email();
+    ri.state = REVIEWER;
+    ReviewerResult result = addReviewer(changeId, ri);
+    assertThat(result.input).isEqualTo(user.email());
+    assertThat(result.confirm).isNull();
+    assertThat(result.error).isNull();
+    assertThat(result.reviewers).hasSize(1);
+
+    // Add an email address that has no account to CC
+    ReviewerInput ccInput = new ReviewerInput();
+    ccInput.reviewer = testEmailAddress;
+    ccInput.state = CC;
+    ReviewerResult resultCC = addReviewer(changeId, ccInput, SC_BAD_REQUEST);
+    assertThat(resultCC.error).contains("Account '" + testEmailAddress + "' not found");
+    assertThat(resultCC.error)
+        .contains(testEmailAddress + " does not identify a registered user or group");
+  }
+
+  @Test
+  public void addReviewerEmailWithoutAccount() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String testEmailAddress = "email@without.account";
+
+    // Add a reviewer without an account
+    ReviewerInput ri = new ReviewerInput();
+    ri.reviewer = testEmailAddress;
+    ri.state = REVIEWER;
+    ReviewerResult result = addReviewer(changeId, ri, SC_BAD_REQUEST);
+    assertThat(result.error).contains("Account '" + testEmailAddress + "' not found");
+    assertThat(result.error)
+        .contains(testEmailAddress + " does not identify a registered user or group");
+  }
+
+  @Test
   public void addCcGroup() throws Exception {
     List<TestAccount> users = createAccounts(6, "addCcGroup");
     List<String> usernames = new ArrayList<>(6);
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
index c81c945..0fb6b9e 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectCacheIT.java
@@ -143,4 +143,17 @@
     inMemoryProjectCache.get(Project.nameKey("foo")); // Another invocation
     assertThat(inMemoryProjectCache.stats().missCount()).isEqualTo(initialNumMisses + 1);
   }
+
+  @Test
+  public void invalidatesNegativeCachingAfterProjectCreation() throws Exception {
+    long initialNumMisses = inMemoryProjectCache.stats().missCount();
+    assertThat(inMemoryProjectCache.get(Project.nameKey(name("foo")))).isEmpty();
+    assertThat(inMemoryProjectCache.stats().missCount())
+        .isEqualTo(initialNumMisses + 1); // Negative voting cached
+    Project.NameKey newProjectName =
+        createProjectOverAPI("foo", allProjects, true, /* submitType= */ null);
+    assertThat(inMemoryProjectCache.get(newProjectName)).isPresent(); // Another invocation
+    assertThat(inMemoryProjectCache.stats().missCount())
+        .isEqualTo(initialNumMisses + 3); // Two eviction happened during the project creation
+  }
 }
diff --git a/javatests/com/google/gerrit/index/query/AndSourceTest.java b/javatests/com/google/gerrit/index/query/AndSourceTest.java
index 068ae8c..6f20bd7 100644
--- a/javatests/com/google/gerrit/index/query/AndSourceTest.java
+++ b/javatests/com/google/gerrit/index/query/AndSourceTest.java
@@ -20,15 +20,24 @@
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.Lists;
+import com.google.gerrit.index.IndexConfig;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testing.ConfigSuite;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 
+@RunWith(ConfigSuite.class)
 public class AndSourceTest extends PredicateTest {
+
+  @ConfigSuite.Parameter public Config config;
+
   @Test
   public void ensureLowerCostPredicateRunsFirst() {
     TestDataSourcePredicate p1 = new TestDataSourcePredicate("predicate1", "foo", 10, 10);
     TestDataSourcePredicate p2 = new TestDataSourcePredicate("predicate2", "foo", 1, 10);
-    AndSource<String> andSource = new AndSource<>(Lists.newArrayList(p1, p2), null);
+    AndSource<String> andSource =
+        new AndSource<>(Lists.newArrayList(p1, p2), IndexConfig.fromConfig(config).build());
     assertFalse(andSource.match("bar"));
     assertFalse(p1.ranMatch);
     assertTrue(p2.ranMatch);
diff --git a/javatests/com/google/gerrit/index/query/PredicateTest.java b/javatests/com/google/gerrit/index/query/PredicateTest.java
index d789201..1853d98 100644
--- a/javatests/com/google/gerrit/index/query/PredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/PredicateTest.java
@@ -14,10 +14,32 @@
 
 package com.google.gerrit.index.query;
 
+import com.google.gerrit.testing.ConfigSuite;
+import org.eclipse.jgit.lib.Config;
 import org.junit.Ignore;
 
 @Ignore
 public abstract class PredicateTest {
+
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    return com.google.gerrit.testing.IndexConfig.create();
+  }
+
+  @ConfigSuite.Config
+  public static Config searchAfterPaginationType() {
+    Config config = defaultConfig();
+    config.setString("index", null, "paginationType", "SEARCH_AFTER");
+    return config;
+  }
+
+  @ConfigSuite.Config
+  public static Config nonePaginationType() {
+    Config config = defaultConfig();
+    config.setString("index", null, "paginationType", "NONE");
+    return config;
+  }
+
   @SuppressWarnings("ProtectedMembersInFinalClass")
   protected static class TestDataSourcePredicate extends TestMatchablePredicate<String>
       implements DataSource<String> {
diff --git a/polygerrit-ui/app/styles/gr-modal-styles.ts b/polygerrit-ui/app/styles/gr-modal-styles.ts
index b1bcf51..a8b5dc4 100644
--- a/polygerrit-ui/app/styles/gr-modal-styles.ts
+++ b/polygerrit-ui/app/styles/gr-modal-styles.ts
@@ -28,3 +28,13 @@
     opacity: var(--modal-opacity, 0.6);
   }
 `;
+
+const $_documentContainer = document.createElement('template');
+$_documentContainer.innerHTML = `<dom-module id="gr-modal-styles">
+  <template>
+    <style>
+      ${modalStyles.cssText}
+    </style>
+  </template>
+</dom-module>`;
+document.head.appendChild($_documentContainer.content);