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