Add 'parentof' operator that matches parent changes for a change

The 'parentof:<change>' search operator finds the parent changes
for the given change. This operator will return immediate parents
and will not return grand parents or higher level ancestors of the
given change.

It is possible for the CI systems to look at tasks for a change
to determine whether to pick the change for further processing (such
as merging) or not. In few scenarios, we may want to impose a policy
on CI such that it should not pick a change if the parent change is
in certain state. It is useful to configure task plugin to utilize
'parentof' so as to define a task which fails if the parent change
is in Submitted state. This helps in imposing flexible policies for
CI systems through tasks.

Change-Id: Ibbe6bc4d539f43d658ddee014a9ec288ac2c2667
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index e02dc21..52c282e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -190,6 +190,13 @@
 +
 Changes occurring in projects starting with 'PREFIX'.
 
+[[parentof]]
+parentof:'ID'::
+Changes which are parent to the change specified by 'ID'. Change 'ID' can be
+specified as a legacy numerical 'ID' such as 15183, or a Change-Id that can be
+picked from the commit message. This operator will return immediate parents
+and will not return grand parents or higher level ancestors of the given change.
+
 [[parentproject]]
 parentproject:'PROJECT'::
 +
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 68a90d2..4e3edcd 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -173,6 +173,7 @@
   public static final String FIELD_MESSAGE = "message";
   public static final String FIELD_OWNER = "owner";
   public static final String FIELD_OWNERIN = "ownerin";
+  public static final String FIELD_PARENTOF = "parentof";
   public static final String FIELD_PARENTPROJECT = "parentproject";
   public static final String FIELD_PATH = "path";
   public static final String FIELD_PENDING_REVIEWER = "pendingreviewer";
@@ -735,6 +736,16 @@
   }
 
   @Operator
+  public Predicate<ChangeData> parentof(String value) throws QueryParseException {
+    List<ChangeData> changes = parseChangeData(value);
+    List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
+    for (ChangeData c : changes) {
+      or.add(new ParentOfPredicate(value, c, args.repoManager));
+    }
+    return Predicate.or(or);
+  }
+
+  @Operator
   public Predicate<ChangeData> parentproject(String name) {
     return new ParentProjectPredicate(args.projectCache, args.childProjects, name);
   }
@@ -1560,14 +1571,18 @@
   }
 
   private List<Change> parseChange(String value) throws QueryParseException {
+    return asChanges(parseChangeData(value));
+  }
+
+  private List<ChangeData> parseChangeData(String value) throws QueryParseException {
     if (PAT_LEGACY_ID.matcher(value).matches()) {
       Optional<Change.Id> id = Change.Id.tryParse(value);
       if (!id.isPresent()) {
         throw error("Invalid change id " + value);
       }
-      return asChanges(args.queryProvider.get().byLegacyChangeId(id.get()));
+      return args.queryProvider.get().byLegacyChangeId(id.get());
     } else if (PAT_CHANGE_ID.matcher(value).matches()) {
-      List<Change> changes = asChanges(args.queryProvider.get().byKeyPrefix(parseChangeId(value)));
+      List<ChangeData> changes = args.queryProvider.get().byKeyPrefix(parseChangeId(value));
       if (changes.isEmpty()) {
         throw error("Change " + value + " not found");
       }
diff --git a/java/com/google/gerrit/server/query/change/ParentOfPredicate.java b/java/com/google/gerrit/server/query/change/ParentOfPredicate.java
new file mode 100644
index 0000000..e48d586
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/ParentOfPredicate.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.OperatorPredicate;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import java.io.IOException;
+import java.util.Set;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class ParentOfPredicate extends OperatorPredicate<ChangeData>
+    implements Matchable<ChangeData> {
+  protected final Set<RevCommit> parents;
+
+  public ParentOfPredicate(String value, ChangeData change, GitRepositoryManager repoManager) {
+    super(ChangeQueryBuilder.FIELD_PARENTOF, value);
+    this.parents = getParents(change, repoManager);
+  }
+
+  @Override
+  public boolean match(ChangeData changeData) {
+    return changeData.patchSets().stream().anyMatch(ps -> parents.contains(ps.commitId()));
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+
+  protected Set<RevCommit> getParents(ChangeData change, GitRepositoryManager repoManager) {
+    PatchSet ps = change.currentPatchSet();
+    try (Repository repo = repoManager.openRepository(change.project());
+        RevWalk walk = new RevWalk(repo)) {
+      RevCommit c = walk.parseCommit(ps.commitId());
+      return Sets.newHashSet(c.getParents());
+    } catch (IOException e) {
+      throw new StorageException(
+          String.format(
+              "Loading commit %s for ps %d of change %d failed.",
+              ps.commitId(), ps.id().get(), ps.id().changeId().get()),
+          e);
+    }
+  }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 7efcb4b..48bd321 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -705,6 +705,24 @@
   }
 
   @Test
+  public void byParentOf() throws Exception {
+    TestRepository<Repo> repo1 = createProject("repo1");
+    RevCommit commit1 = repo1.parseBody(repo1.commit().message("message").create());
+    Change change1 = insert(repo1, newChangeForCommit(repo1, commit1));
+    RevCommit commit2 = repo1.parseBody(repo1.commit(commit1));
+    Change change2 = insert(repo1, newChangeForCommit(repo1, commit2));
+    RevCommit commit3 = repo1.parseBody(repo1.commit(commit1, commit2));
+    Change change3 = insert(repo1, newChangeForCommit(repo1, commit3));
+
+    assertQuery("parentof:" + change1.getId().get());
+    assertQuery("parentof:" + change1.getKey().get());
+    assertQuery("parentof:" + change2.getId().get(), change1);
+    assertQuery("parentof:" + change2.getKey().get(), change1);
+    assertQuery("parentof:" + change3.getId().get(), change2, change1);
+    assertQuery("parentof:" + change3.getKey().get(), change2, change1);
+  }
+
+  @Test
   public void byParentProject() throws Exception {
     TestRepository<Repo> repo1 = createProject("repo1");
     TestRepository<Repo> repo2 = createProject("repo2", "repo1");
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
index 43f8a2b..97d5271 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.ts
@@ -93,6 +93,7 @@
   'onlyextensions:',
   'owner:',
   'ownerin:',
+  'parentof:',
   'parentproject:',
   'project:',
   'projects:',