Add support for --depends-on--all query option

Add --depends-on--all option to gerrit query SSH command and REST API.
This option will add a plugin section to the output which displays all
the depends-ons from the latest Depends-on comment on the change. This
option helps in building a depends-on web UI in future. Also add
functional tests and documentation.

Change-Id: I713f7765f47d115d499299f449fe08a70dd1a532
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/ChangeMessageStore.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/ChangeMessageStore.java
index 5bfccd5..6901c90 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/depends/on/ChangeMessageStore.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/ChangeMessageStore.java
@@ -85,7 +85,11 @@
    *
    * <p>return empty set means no dependencies found.
    */
-  public Set<DependsOn> load(Change.Id cid) {
+  public Set<DependsOn> load(Change.Id cid) throws StorageException {
+    return loadWithOrder(cid).stream().collect(Collectors.toSet());
+  }
+
+  public List<DependsOn> loadWithOrder(Change.Id cid) throws StorageException {
     ChangeNotes changeNote = changeNotesFactory.createCheckedUsingIndexLookup(cid);
     List<ChangeMessage> messages = cmUtil.byChange(changeNote);
     List<ChangeMessage> sortedChangeMessages =
@@ -93,12 +97,12 @@
             .sorted(Comparator.comparing(ChangeMessage::getWrittenOn).reversed())
             .collect(Collectors.toCollection(ArrayList::new));
     for (ChangeMessage message : sortedChangeMessages) {
-      Optional<Set<DependsOn>> deps = Comment.from(message.getMessage());
+      Optional<List<DependsOn>> deps = Comment.from(message.getMessage());
       if (deps.isPresent()) {
         return deps.get();
       }
     }
-    return Collections.emptySet();
+    return Collections.emptyList();
   }
 
   /** If needed, create a comment on the change with a DependsOn for the dependencies. */
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOnAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOnAttributeFactory.java
new file mode 100644
index 0000000..3badb76
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOnAttributeFactory.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2022 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.googlesource.gerrit.plugins.depends.on;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.common.PluginDefinedInfo;
+import com.google.gerrit.server.DynamicOptions;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
+import com.google.gerrit.server.query.change.ChangeData;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DependsOnAttributeFactory implements ChangePluginDefinedInfoFactory {
+  private static final Logger log = LoggerFactory.getLogger(DependsOnAttributeFactory.class);
+  protected static final String FAILED_TO_LOAD_DEPENDS_ON = "Failed to load Depends-on";
+
+  protected final ChangeMessageStore changeMessageStore;
+
+  @Inject
+  DependsOnAttributeFactory(ChangeMessageStore changeMessageStore) {
+    this.changeMessageStore = changeMessageStore;
+  }
+
+  @Override
+  public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos(
+      Collection<ChangeData> cds, DynamicOptions.BeanProvider beanProvider, String plugin) {
+    if (!((Module.MyQueryOptions) beanProvider.getDynamicBean(plugin)).all) {
+      return Collections.emptyMap();
+    }
+    Map<Change.Id, PluginDefinedInfo> dependsOnAttributesByChange = new HashMap<>();
+    for (ChangeData changeData : cds) {
+      try {
+        List<DependsOn> dependsOns = changeMessageStore.loadWithOrder(changeData.getId());
+        if (dependsOns.size() > 0) {
+          DependsOnPluginAttributes dependsOnPluginAttributes = new DependsOnPluginAttributes();
+          dependsOnPluginAttributes.addDependsOns(dependsOns);
+          dependsOnAttributesByChange.put(changeData.getId(), dependsOnPluginAttributes);
+        }
+      } catch (StorageException e) {
+        log.error(FAILED_TO_LOAD_DEPENDS_ON, e);
+        PluginDefinedInfo pluginDefinedInfo = new PluginDefinedInfo();
+        pluginDefinedInfo.message = FAILED_TO_LOAD_DEPENDS_ON;
+        dependsOnAttributesByChange.put(changeData.getId(), pluginDefinedInfo);
+      }
+    }
+    return dependsOnAttributesByChange;
+  }
+
+  protected static class DependsOnPluginAttributes extends PluginDefinedInfo {
+    public List<DependsOnAttribute> dependsOns = new ArrayList<>();
+
+    public void addDependsOns(List<DependsOn> dependsOns) {
+      for (DependsOn dependsOn : dependsOns) {
+        this.dependsOns.add(new DependsOnAttribute(dependsOn));
+      }
+    }
+  }
+
+  protected static class DependsOnAttribute {
+    @Nullable protected Integer changeNumber;
+    @Nullable protected String unresolved;
+
+    public DependsOnAttribute(DependsOn dependsOn) {
+      if (dependsOn.isResolved()) {
+        changeNumber = dependsOn.id().get();
+      } else {
+        unresolved = dependsOn.key().get();
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/Module.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/Module.java
index 21644c1..b6eb1e1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/depends/on/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/Module.java
@@ -14,16 +14,27 @@
 
 package com.googlesource.gerrit.plugins.depends.on;
 
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.validators.CommentValidator;
 import com.google.gerrit.server.DynamicOptions.DynamicBean;
+import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory;
 import com.google.gerrit.server.events.EventListener;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
 import com.google.gerrit.server.restapi.change.GetChange;
 import com.google.gerrit.server.restapi.change.QueryChanges;
 import com.google.gerrit.sshd.commands.Query;
 import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.depends.on.extensions.DependencyResolver;
+import java.util.Set;
+import org.kohsuke.args4j.Option;
 
 public class Module extends AbstractModule {
   @Override
@@ -32,15 +43,39 @@
         .annotatedWith(Exports.named(InDependsOnOperator.FIELD))
         .to(InDependsOnOperator.class);
     DynamicSet.bind(binder(), EventListener.class).to(CoreListener.class);
-    bind(DynamicBean.class)
-        .annotatedWith(Exports.named(GetChange.class))
-        .to(ChangeMessageStore.class);
-    bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(ChangeMessageStore.class);
+    bind(ChangePluginDefinedInfoFactory.class)
+        .annotatedWith(Exports.named("depends-ons"))
+        .to(DependsOnAttributeFactory.class);
+    bind(DynamicBean.class).annotatedWith(Exports.named(GetChange.class)).to(MyQueryOptions.class);
+    bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyQueryOptions.class);
     bind(DynamicBean.class)
         .annotatedWith(Exports.named(QueryChanges.class))
-        .to(ChangeMessageStore.class);
+        .to(MyQueryOptions.class);
     bind(CommentValidator.class)
         .annotatedWith(Exports.named(DependsOnCommentValidator.class.getSimpleName()))
         .to(DependsOnCommentValidator.class);
   }
+
+  public static class MyQueryOptions implements DependencyResolver {
+    @Option(name = "--all", usage = "Include all depends-on in the output")
+    public boolean all = false;
+
+    protected final ChangeMessageStore changeMessageStore;
+
+    @Inject
+    public MyQueryOptions(ChangeMessageStore changeMessageStore) {
+      this.changeMessageStore = changeMessageStore;
+    }
+
+    @Override
+    public boolean resolveDependencies(PatchSet.Id patchSetId, Set<Set<BranchNameKey>> deliverables)
+        throws InvalidChangeOperationException, StorageException, NoSuchChangeException {
+      return changeMessageStore.resolveDependencies(patchSetId, deliverables);
+    }
+
+    @Override
+    public boolean hasUnresolvedDependsOn(Change.Id changeId) throws StorageException {
+      return changeMessageStore.hasUnresolvedDependsOn(changeId);
+    }
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/formats/Comment.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/formats/Comment.java
index e911560..8b35bcd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/depends/on/formats/Comment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/formats/Comment.java
@@ -16,6 +16,7 @@
 
 import com.googlesource.gerrit.plugins.depends.on.DependsOn;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -30,7 +31,7 @@
   protected static final Pattern DEPENDS_ON_PATTERN = Pattern.compile("^Depends-on:(.*)$");
 
   /** return empty Optional instance means no dependencies found */
-  public static Optional<Set<DependsOn>> from(String comment) {
+  public static Optional<List<DependsOn>> from(String comment) {
     for (String line : comment.split("\n", -1)) {
       Matcher match = DEPENDS_ON_PATTERN.matcher(line);
       if (match.find()) {
@@ -41,7 +42,7 @@
             Arrays.stream(changes.split("\\s+", -1)) // -> ["1234", "4444"]
                 .filter(c -> !c.isEmpty())
                 .map(c -> DependsOn.create(c))
-                .collect(Collectors.toSet()));
+                .collect(Collectors.toList()));
       }
     }
     return Optional.empty();
diff --git a/src/main/resources/Documentation/change-search-attributes.md b/src/main/resources/Documentation/change-search-attributes.md
new file mode 100644
index 0000000..cd8e7cf
--- /dev/null
+++ b/src/main/resources/Documentation/change-search-attributes.md
@@ -0,0 +1,45 @@
+Search Attributes
+=================
+
+Change Query Output
+-------------------
+It is possible to add a dependsOns section to the query output of changes
+using the @PLUGIN@ plugin switches. The following switches are available:
+
+**\-\-@PLUGIN@\-\-all**
+
+This switch is meant to be used to show all depends-ons from the latest
+Depends-on comment on the change. The switch output order matches the
+order from the change comment.
+
+When a change has one or more depends-on associated with it, query output will
+have a "dependsOns" section under the plugins section like below:
+
+```
+  $ ssh -x -p 29418 example.com gerrit query --@PLUGIN@--all \
+        --format JSON change:144193 | head -1 | json_pp
+  {
+     "id" : "Ifc577c2660c26220c39df57627ca1053c3f2067c",
+     ...
+     "plugins" : [
+        {
+           "name" : "@PLUGIN@",
+           "dependsOns" : [
+              {
+                "changeNumber": 732
+              },
+              {
+                "changeNumber": 733
+              },
+              {
+                "unresolved": "Ieace383c14de79bf202c85063d5a46a0580724dd"
+              },
+              {
+                "changeNumber": 734
+              }
+           ],
+           "name": "depends-on"
+        }
+     ]
+  }
+```
diff --git a/src/test/java/com/googlesource/gerrit/plugins/depends/on/DependsOnParsingTest.java b/src/test/java/com/googlesource/gerrit/plugins/depends/on/DependsOnParsingTest.java
index 52de2a7..491d572 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/depends/on/DependsOnParsingTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/depends/on/DependsOnParsingTest.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.testing.InMemoryModule;
 import com.googlesource.gerrit.plugins.depends.on.formats.Comment;
+import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 import junit.framework.TestCase;
 import org.junit.Test;
 
@@ -68,21 +68,21 @@
   @Test
   public void testParseNoneComment() {
     String comment = "My Very Educated Mother Just Served Us Nothing!";
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertFalse(deps.isPresent());
   }
 
   @Test
   public void testParseEmptyComment() {
     String comment = "Depends-on:";
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 0);
   }
 
   @Test
   public void testParseOneNumComment() {
     String comment = "Depends-on:" + NUM;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     for (DependsOn dep : deps.get()) {
       assertTrue(NUM.equals("" + dep.id().get()));
       return;
@@ -93,7 +93,7 @@
   @Test
   public void testParseOneKeyComment() {
     String comment = "Depends-on:" + KEY;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     for (DependsOn dep : deps.get()) {
       assertTrue(KEY.equals("" + dep.key().get()));
       return;
@@ -104,7 +104,7 @@
   @Test
   public void testParseTwoNumsComment() {
     String comment = "Depends-on:" + NUM + " " + NUM2;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 2);
     int found = 0;
     for (DependsOn dep : deps.get()) {
@@ -117,7 +117,7 @@
   @Test
   public void testParseTwoKeyComments() {
     String comment = "Depends-on:" + KEY + " " + KEY2;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 2);
     int found = 0;
     for (DependsOn dep : deps.get()) {
@@ -130,7 +130,7 @@
   @Test
   public void testParseNumAndKeyComment() {
     String comment = "Depends-on:" + NUM + " " + KEY;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 2);
     int found = 0;
     for (DependsOn dep : deps.get()) {
@@ -146,7 +146,7 @@
 
   public void testParseTwoNumsCommaComment() {
     String comment = "Depends-on:" + NUM + "," + NUM2;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 2);
     int found = 0;
     for (DependsOn dep : deps.get()) {
@@ -158,7 +158,7 @@
 
   public void testParseTwoNumsCommaSpaceComment() {
     String comment = "Depends-on:" + NUM + ", " + NUM2;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 2);
     int found = 0;
     for (DependsOn dep : deps.get()) {
@@ -170,7 +170,7 @@
 
   public void testParseTwoNumsWhiteComment() {
     String comment = "Depends-on:" + NUM + ", \t" + NUM2;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 2);
     int found = 0;
     for (DependsOn dep : deps.get()) {
@@ -183,7 +183,7 @@
   public void testParseTwoNumsNewLineWhiteComment() {
     // Should stop processing at newline
     String comment = "Depends-on:" + NUM + ", \t\n" + NUM2;
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 1);
     for (DependsOn dep : deps.get()) {
       assertTrue(NUM.equals("" + dep.id().get()));
@@ -192,7 +192,7 @@
 
   public void testParseEmbeddedComment() {
     String comment = "Patch Set 2:\n\nDepends-on:" + NUM + " " + NUM2 + "\nHey\n";
-    Optional<Set<DependsOn>> deps = Comment.from(comment);
+    Optional<List<DependsOn>> deps = Comment.from(comment);
     assertTrue(deps.get().size() == 2);
     int found = 0;
     for (DependsOn dep : deps.get()) {
diff --git a/test/test_dependson.sh b/test/test_dependson.sh
index cc68ced..7e78684 100755
--- a/test/test_dependson.sh
+++ b/test/test_dependson.sh
@@ -171,7 +171,7 @@
 GIT_DIR=$REPO_DIR/.git
 FILE_A=$REPO_DIR/fileA
 
-# ------------------------- Depends-on Test ---------------------------
+# ------------------------- Depends-on propagation Test ---------------------------
 base_change=$(create_change "$SRC_REF_BRANCH" "$FILE_A") || exit
 src_change=$(create_change "$SRC_REF_BRANCH" "$FILE_A") || exit
 gssh gerrit review --message \'"Depends-on: $base_change"\' "$src_change",1
@@ -180,7 +180,6 @@
 actual=$(get_depends_on_tag "$dest_change")
 result_out "propagate depends-on" "$expected" "$actual"
 
-
 # ------------------------- Depends-on comment validator test ---------------------------
 change=$(create_change "$SRC_REF_BRANCH" "$FILE_A") || exit
 commit=$(mygit log -1 --pretty=format:"%H")
@@ -213,4 +212,19 @@
         '{"comments": {"/PATCHSET_LEVEL":[{"message": "Depends-on: 10 30"}]}}')
 result_out "depends-on patchset level comment - REST" "$expected" "$out"
 
+# ------------------------- Depends-on query Test ---------------------------
+change=$(create_change "$SRC_REF_BRANCH" "$FILE_A") || exit
+gssh gerrit review --message \
+    \'"Depends-on: 10 30 Ieace383c14de79bf202c85063d5a46a0580724dd 20"\' "$change",1
+out=$(query "$change" --depends-on--all | jq --raw-output '.plugins[0].dependsOns')
+result "depends-on query"
+result_out "depends-on query output 1" "10" "$(echo "$out" | jq '.[0].changeNumber')"
+result_out "depends-on query output 2" "30" "$(echo "$out" | jq '.[1].changeNumber')"
+result_out "depends-on query output 3" "Ieace383c14de79bf202c85063d5a46a0580724dd" \
+    "$(echo "$out" | jq --raw-output '.[2].unresolved')"
+result_out "depends-on query output 4" "20" "$(echo "$out" | jq '.[3].changeNumber')"
+result_out "depends-on query output datatype" "number number string number" \
+    "$(echo "$out" | jq --raw-output 'map(select(.changeNumber!=null).changeNumber,
+        select(.unresolved!=null).unresolved | type) | join(" ")')"
+
 exit $RESULT