Support indexing changes by author and committer

Index changes by the author and committer of the change's current
patch set.

Support searching by exact email address, or by parts of the name
or email address.

Feature: Issue 3333
Change-Id: Id9f963f9453030eb1376d3025dd5cfd5d86e5151
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index adf77cf..eda4b5d 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -367,6 +367,19 @@
 Changes where 'USER' has commented on the change more recently than the
 last update (comment or patch set) from the change owner.
 
+[[author]]
+author:'AUTHOR'::
++
+Changes where 'AUTHOR' is the author of the current patch set. 'AUTHOR' may be
+the author's exact email address, or part of the name or email address.
+
+[[committer]]
+committer:'COMMITTER'::
++
+Changes where 'COMMITTER' is the committer of the current patch set.
+'COMMITTER' may be the committer's exact email address, or part of the name or
+email address.
+
 
 == Argument Quoting
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 20afa19..6c2fd04 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -72,6 +72,8 @@
     suggestions.add("owner:");
     suggestions.add("owner:self");
     suggestions.add("ownerin:");
+    suggestions.add("author:");
+    suggestions.add("committer:");
 
     suggestions.add("reviewer:");
     suggestions.add("reviewer:self");
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 5d7229a..e711306 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
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 
+import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
@@ -38,12 +39,14 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.protobuf.CodedOutputStream;
 
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.FooterLine;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -404,6 +407,64 @@
         }
       };
 
+  private static Set<String> getPersonParts(PersonIdent person) {
+    if (person == null) {
+      return ImmutableSet.of();
+    }
+    HashSet<String> parts = Sets.newHashSet();
+    String email = person.getEmailAddress().toLowerCase();
+    parts.add(email);
+    parts.addAll(Arrays.asList(email.split("@")));
+    Splitter s = Splitter.on(CharMatcher.anyOf("@.- ")).omitEmptyStrings();
+    Iterables.addAll(parts, s.split(email));
+    Iterables.addAll(parts, s.split(person.getName().toLowerCase()));
+    return parts;
+  }
+
+  public static Set<String> getAuthorParts(ChangeData cd) throws OrmException {
+    try {
+      return getPersonParts(cd.getAuthor());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  public static Set<String> getCommitterParts(ChangeData cd) throws OrmException {
+    try {
+      return getPersonParts(cd.getCommitter());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  /**
+   * The exact email address, or any part of the author name or email address,
+   * in the current patch set.
+   */
+  public static final FieldDef<ChangeData, Iterable<String>> AUTHOR =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_AUTHOR, FieldType.FULL_TEXT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getAuthorParts(input);
+        }
+      };
+
+  /**
+   * The exact email address, or any part of the committer name or email address,
+   * in the current patch set.
+   */
+  public static final FieldDef<ChangeData, Iterable<String>> COMMITTER =
+      new FieldDef.Repeatable<ChangeData, String>(
+          ChangeQueryBuilder.FIELD_COMMITTER, FieldType.FULL_TEXT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return getCommitterParts(input);
+        }
+      };
+
   public static class ChangeProtoField extends FieldDef.Single<ChangeData, byte[]> {
     public static final ProtobufCodec<Change> CODEC =
         CodecFactory.encoder(Change.class);
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 4915a663..a8a97a8 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
@@ -308,6 +308,7 @@
       ChangeField.REVIEWEDBY,
       ChangeField.EXACT_COMMIT);
 
+  @Deprecated
   static final Schema<ChangeData> V23 = schema(
       ChangeField.LEGACY_ID2,
       ChangeField.ID,
@@ -341,6 +342,40 @@
       ChangeField.REVIEWEDBY,
       ChangeField.EXACT_COMMIT);
 
+  static final Schema<ChangeData> V24 = schema(
+      ChangeField.LEGACY_ID2,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.EXACT_TOPIC,
+      ChangeField.FUZZY_TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET,
+      ChangeField.GROUP,
+      ChangeField.EDITBY,
+      ChangeField.REVIEWEDBY,
+      ChangeField.EXACT_COMMIT,
+      ChangeField.AUTHOR,
+      ChangeField.COMMITTER);
 
   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/AuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
new file mode 100644
index 0000000..193a061
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -0,0 +1,39 @@
+// 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 static com.google.gerrit.server.index.ChangeField.AUTHOR;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_AUTHOR;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+public class AuthorPredicate extends IndexPredicate<ChangeData>  {
+  AuthorPredicate(String value) {
+    super(AUTHOR, FIELD_AUTHOR, value);
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    return ChangeField.getAuthorParts(object).contains(
+        getValue().toLowerCase());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 0523d73..8061a26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -62,6 +62,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
@@ -317,6 +318,8 @@
   private Boolean mergeable;
   private Set<Account.Id> editsByUser;
   private Set<Account.Id> reviewedBy;
+  private PersonIdent author;
+  private PersonIdent committer;
 
   @AssistedInject
   private ChangeData(
@@ -620,6 +623,24 @@
     return commitFooters;
   }
 
+  public PersonIdent getAuthor() throws IOException, OrmException {
+    if (author == null) {
+      if (!loadCommitData()) {
+        return null;
+      }
+    }
+    return author;
+  }
+
+  public PersonIdent getCommitter() throws IOException, OrmException {
+    if (committer == null) {
+      if (!loadCommitData()) {
+        return null;
+      }
+    }
+    return committer;
+  }
+
   private boolean loadCommitData() throws OrmException,
       RepositoryNotFoundException, IOException, MissingObjectException,
       IncorrectObjectTypeException {
@@ -633,6 +654,8 @@
       RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
       commitMessage = c.getFullMessage();
       commitFooters = c.getFooterLines();
+      author = c.getAuthorIdent();
+      committer = c.getCommitterIdent();
     }
     return true;
   }
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 c6ffc27..776a7f6 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
@@ -91,12 +91,14 @@
   public static final String FIELD_ADDED = "added";
   public static final String FIELD_AFTER = "after";
   public static final String FIELD_AGE = "age";
+  public static final String FIELD_AUTHOR = "author";
   public static final String FIELD_BEFORE = "before";
   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_COMMITTER = "committer";
   public static final String FIELD_CONFLICTS = "conflicts";
   public static final String FIELD_DELETED = "deleted";
   public static final String FIELD_DELTA = "delta";
@@ -843,6 +845,16 @@
     throw new QueryParseException("Unknown named destination: " + name);
   }
 
+  @Operator
+  public Predicate<ChangeData> author(String who) {
+    return new AuthorPredicate(who);
+  }
+
+  @Operator
+  public Predicate<ChangeData> committer(String who) {
+    return new CommitterPredicate(who);
+  }
+
   @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/CommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
new file mode 100644
index 0000000..e5d9529
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -0,0 +1,39 @@
+// 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 static com.google.gerrit.server.index.ChangeField.COMMITTER;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_COMMITTER;
+
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gwtorm.server.OrmException;
+
+public class CommitterPredicate extends IndexPredicate<ChangeData>  {
+  CommitterPredicate(String value) {
+    super(COMMITTER, FIELD_COMMITTER, value);
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    return ChangeField.getCommitterParts(object).contains(
+        getValue().toLowerCase());
+  }
+
+  @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 27c3443..499caa2 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
@@ -380,6 +380,52 @@
   }
 
   @Test
+  public void byAuthor() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+
+    // By exact email address
+    assertQuery("author:jauthor@example.com", change1);
+
+    // By email address part
+    assertQuery("author:jauthor", change1);
+    assertQuery("author:example", change1);
+    assertQuery("author:example.com", change1);
+
+    // By name part
+    assertQuery("author:Author", change1);
+
+    // By non-existing email address / name / part
+    assertQuery("author:jcommitter@example.com");
+    assertQuery("author:somewhere.com");
+    assertQuery("author:jcommitter");
+    assertQuery("author:Committer");
+  }
+
+  @Test
+  public void byCommitter() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, userId.get(), null).insert();
+
+    // By exact email address
+    assertQuery("committer:jcommitter@example.com", change1);
+
+    // By email address part
+    assertQuery("committer:jcommitter", change1);
+    assertQuery("committer:example", change1);
+    assertQuery("committer:example.com", change1);
+
+    // By name part
+    assertQuery("committer:Committer", change1);
+
+    // By non-existing email address / name / part
+    assertQuery("committer:jauthor@example.com");
+    assertQuery("committer:somewhere.com");
+    assertQuery("committer:jauthor");
+    assertQuery("committer:Author");
+  }
+
+  @Test
   public void byOwnerIn() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();