Add "commentby" search operator to search authors of comments

Like the "comment" field, this includes both top-level and inline
comments, although in practice the REST API doesn't allow adding
inline comments without a corresponding top-level comment.

Change-Id: Icc1ca78d8de46b1dbba0cff378fa53b94f9af1a2
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 83accb8..be4f890 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -326,6 +326,13 @@
 Valid relations are >=, >, <=, <, or no relation, which will match if the
 number of lines is exactly equal.
 
+[[commentby]]
+commentby:'USER'::
++
+Changes containing a top-level or inline comment by 'USER'. The special
+case of `commentby:self` will find changes where the caller has
+commented.
+
 
 == Argument Quoting
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 2993739..821343f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -44,6 +44,7 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -498,6 +499,26 @@
         }
       };
 
+  /** Users who have commented on this change. */
+  public static final FieldDef<ChangeData, Iterable<Integer>> COMMENTBY =
+      new FieldDef.Repeatable<ChangeData, Integer>(
+          ChangeQueryBuilder.FIELD_COMMENTBY, FieldType.INTEGER, false) {
+        @Override
+        public Iterable<Integer> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          Set<Integer> r = new HashSet<>();
+          for (ChangeMessage m : input.messages()) {
+            if (m.getAuthor() != null) {
+              r.add(m.getAuthor().get());
+            }
+          }
+          for (PatchLineComment c : input.publishedComments()) {
+            r.add(c.getAuthor().get());
+          }
+          return r;
+        }
+      };
+
   private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
       throws OrmException {
     List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 05bf9bd..ef6d626 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -115,6 +115,34 @@
       ChangeField.DELTA,
       ChangeField.HASHTAG);
 
+  static final Schema<ChangeData> V15 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY);
+
   private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(ImmutableList.copyOf(fields));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index c9d7e6cd..5460296 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -88,6 +88,7 @@
   public static final String FIELD_BRANCH = "branch";
   public static final String FIELD_CHANGE = "change";
   public static final String FIELD_COMMENT = "comment";
+  public static final String FIELD_COMMENTBY = "commentby";
   public static final String FIELD_COMMIT = "commit";
   public static final String FIELD_CONFLICTS = "conflicts";
   public static final String FIELD_DELETED = "deleted";
@@ -743,6 +744,17 @@
     return new DeltaPredicate(value);
   }
 
+  @Operator
+  public Predicate<ChangeData> commentby(String who)
+      throws QueryParseException, OrmException {
+    Set<Account.Id> m = parseAccount(who);
+    List<OwnerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new OwnerPredicate(id));
+    }
+    return Predicate.or(p);
+  }
+
   @Override
   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
new file mode 100644
index 0000000..dee7086
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2015 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.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.Objects;
+
+class CommentByPredicate extends IndexPredicate<ChangeData> {
+  private final Account.Id id;
+
+  CommentByPredicate(Account.Id id) {
+    super(ChangeField.COMMENTBY, id.toString());
+    this.id = id;
+  }
+
+  Account.Id getAccountId() {
+    return id;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    for (ChangeMessage m : cd.messages()) {
+      if (Objects.equals(m.getAuthor(), id)) {
+        return true;
+      }
+    }
+    for (PatchLineComment c : cd.publishedComments()) {
+      if (Objects.equals(c.getAuthor(), id)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 34588fa..d03c5e5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -1091,6 +1092,41 @@
     assertResultEquals(change1, queryOne(q + " visibleto:" + user2.get()));
   }
 
+  @Test
+  public void byCommentBy() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.insert();
+    PatchSet ps1 = ins1.getPatchSet();
+    ChangeInserter ins2 = newChange(repo, null, null, null, null);
+    Change change2 = ins2.insert();
+    PatchSet ps2 = ins2.getPatchSet();
+
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+
+    ReviewInput input = new ReviewInput();
+    input.message = "toplevel";
+    ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
+    comment.line = 1;
+    comment.message = "inline";
+    input.comments = ImmutableMap.<String, List<ReviewInput.CommentInput>> of(
+        Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput> of(comment));
+    postReview.apply(new RevisionResource(changes.parse(change1.getId()), ps1),
+        input);
+
+    input = new ReviewInput();
+    input.message = "toplevel";
+    postReview.apply(new RevisionResource(changes.parse(change2.getId()), ps2),
+        input);
+
+    List<ChangeInfo> results = query("commentby:" + userId.get());
+    assertThat(results).hasSize(2);
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+    assertThat(query("commentby:" + user2)).isEmpty();
+  }
+
   protected ChangeInserter newChange(
       TestRepository<InMemoryRepository> repo,
       @Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,