Add a comment validator that blocks patchset level Depends-on comments

This comment validator is added so that 3.5 users don't accidentally
post Depends-on comment as patchset level comment. Also update docs
and add functional tests.

Change-Id: I145daa6857314049ea776b89ad62a0e4b65a94f9
diff --git a/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOnCommentValidator.java b/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOnCommentValidator.java
new file mode 100644
index 0000000..1c3b3bc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/depends/on/DependsOnCommentValidator.java
@@ -0,0 +1,41 @@
+// 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.common.collect.ImmutableList;
+import com.google.gerrit.extensions.validators.CommentForValidation;
+import com.google.gerrit.extensions.validators.CommentValidationContext;
+import com.google.gerrit.extensions.validators.CommentValidationFailure;
+import com.google.gerrit.extensions.validators.CommentValidator;
+import com.googlesource.gerrit.plugins.depends.on.formats.Comment;
+
+public class DependsOnCommentValidator implements CommentValidator {
+
+  @Override
+  public ImmutableList<CommentValidationFailure> validateComments(
+      CommentValidationContext ctx, ImmutableList<CommentForValidation> comments) {
+    ImmutableList.Builder<CommentValidationFailure> failures = ImmutableList.builder();
+    for (CommentForValidation c : comments) {
+      if (!CommentForValidation.CommentType.CHANGE_MESSAGE.equals(c.getType())
+          && Comment.hasDependsOn(c.getText())) {
+        failures.add(
+            c.failValidation(
+                "Depends-on tags as a patchset level comment are not"
+                    + " supported. See depends-on plugin documentation."));
+      }
+    }
+    return failures.build();
+  }
+}
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 7736734..21644c1 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
@@ -16,6 +16,7 @@
 
 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.events.EventListener;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeOperatorFactory;
@@ -38,5 +39,8 @@
     bind(DynamicBean.class)
         .annotatedWith(Exports.named(QueryChanges.class))
         .to(ChangeMessageStore.class);
+    bind(CommentValidator.class)
+        .annotatedWith(Exports.named(DependsOnCommentValidator.class.getSimpleName()))
+        .to(DependsOnCommentValidator.class);
   }
 }
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 6703965..e911560 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
@@ -47,6 +47,15 @@
     return Optional.empty();
   }
 
+  public static boolean hasDependsOn(String comment) {
+    for (String line : comment.split("\n", -1)) {
+      if (DEPENDS_ON_PATTERN.matcher(line).find()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   public static StringBuilder getMessages(Set<DependsOn> dependsons) {
     StringBuilder dependencies = new StringBuilder("Depends-on:");
     for (DependsOn dep : dependsons) {
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 930a0a6..ba7906f 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -2,14 +2,14 @@
 ========
 
 This plugin provides a way to mark a change dependent on other change(s). To
-mark a change dependent, post a comment with the list of dependencies under a
-change in the Gerrit page (not an inline diff comment) in a single line with
-the format `Depends-on: c1 c2 c3 ....` where c1, c2 and c3 are gerrit change
-numbers. Any number of changes can be provided after the `Depends-on:` tag.
-The `Depends-on:` tag is case-sensitive. Only changes listed in the most
-recent `Depends-on:` tag are considered as valid dependencies and older tags
-are ignored. To remove existing dependencies, a `Depends-on:` tag with no
-changes must be added.
+mark a change dependent, post a change level comment with the list of dependencies
+under a change in the Gerrit page (not an inline diff comment or patchset level comment)
+in a single line with the format `Depends-on: c1 c2 c3 ....` where c1, c2 and c3
+are gerrit change numbers. Any number of changes can be provided after the
+`Depends-on:` tag. The `Depends-on:` tag is case-sensitive. Only changes listed
+in the most recent `Depends-on:` tag are considered as valid dependencies and
+older tags are ignored. To remove existing dependencies, a `Depends-on:` tag with
+no changes must be added.
 
 PROPAGATION
 -----------
diff --git a/test/test_dependson.sh b/test/test_dependson.sh
index bf55203..cc68ced 100755
--- a/test/test_dependson.sh
+++ b/test/test_dependson.sh
@@ -51,6 +51,13 @@
         tail -n +2 | jq --raw-output '._number'
 }
 
+review_change() { # change commit_sha input_json [args...] > API_response
+  local change=$1 commit_sha=$2 data=$3 ; shift 3
+  curl -X POST --netrc --silent --header 'Content-Type: application/json' \
+      "$@" --data "$data" \
+      "http://$SERVER:8080/a/changes/$change/revisions/$commit_sha/review"
+}
+
 create_change() { # branch file [commit_message] > changenum
     local branch=$1 tmpfile=$2 msg=$3 out rtn
     local content=$RANDOM dest=refs/for/$branch
@@ -173,4 +180,37 @@
 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")
+gssh gerrit review --message \
+    \'"Depends-on: 10 30 Ieace383c14de79bf202c85063d5a46a0580724dd 20"\' "$change",1
+result "depends-on change level comment - SSH"
+
+expected=$(cat <<EOF
+error: One or more comments were rejected in validation: Depends-on tags as a \
+patchset level comment are not supported. See depends-on plugin documentation.
+
+fatal: one or more reviews failed; review output above
+EOF
+)
+
+out=$(echo '{"comments": {"/PATCHSET_LEVEL":[{"message": "Depends-on: 10 30"}]}}' | \
+        gssh gerrit review --json "$change",1 2>&1)
+result_out "depends-on patchset level comment - SSH" "$expected" "$out"
+
+review_change "$change" "$commit" '{"message" : "Depends-on: 10 30"}' "--fail" > /dev/null 2>&1
+result "depends-on change level comment - REST"
+
+expected=$(cat <<EOF
+One or more comments were rejected in validation: Depends-on tags as a \
+patchset level comment are not supported. See depends-on plugin documentation.
+EOF
+)
+
+out=$(review_change "$change" "$commit" \
+        '{"comments": {"/PATCHSET_LEVEL":[{"message": "Depends-on: 10 30"}]}}')
+result_out "depends-on patchset level comment - REST" "$expected" "$out"
+
 exit $RESULT