Merge "Fix 'author' and 'committer' search operators"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index 8765607..a3bd230 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -74,10 +74,12 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.FooterLine;
/**
@@ -324,10 +326,36 @@
return SchemaUtil.getPersonParts(cd.getAuthor());
}
+ public static Set<String> getAuthorNameAndEmail(ChangeData cd) throws OrmException, IOException {
+ return getNameAndEmail(cd.getAuthor());
+ }
+
public static Set<String> getCommitterParts(ChangeData cd) throws OrmException, IOException {
return SchemaUtil.getPersonParts(cd.getCommitter());
}
+ public static Set<String> getCommitterNameAndEmail(ChangeData cd)
+ throws OrmException, IOException {
+ return getNameAndEmail(cd.getCommitter());
+ }
+
+ private static Set<String> getNameAndEmail(PersonIdent person) {
+ if (person == null) {
+ return ImmutableSet.of();
+ }
+
+ String name = person.getName().toLowerCase(Locale.US);
+ String email = person.getEmailAddress().toLowerCase(Locale.US);
+
+ StringBuilder nameEmailBuilder = new StringBuilder();
+ PersonIdent.appendSanitized(nameEmailBuilder, name);
+ nameEmailBuilder.append(" <");
+ PersonIdent.appendSanitized(nameEmailBuilder, email);
+ nameEmailBuilder.append('>');
+
+ return ImmutableSet.of(name, email, nameEmailBuilder.toString());
+ }
+
/**
* The exact email address, or any part of the author name or email address, in the current patch
* set.
@@ -335,6 +363,11 @@
public static final FieldDef<ChangeData, Iterable<String>> AUTHOR =
fullText(ChangeQueryBuilder.FIELD_AUTHOR).buildRepeatable(ChangeField::getAuthorParts);
+ /** The exact name, email address and NameEmail of the author. */
+ public static final FieldDef<ChangeData, Iterable<String>> EXACT_AUTHOR =
+ exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR)
+ .buildRepeatable(ChangeField::getAuthorNameAndEmail);
+
/**
* The exact email address, or any part of the committer name or email address, in the current
* patch set.
@@ -342,6 +375,11 @@
public static final FieldDef<ChangeData, Iterable<String>> COMMITTER =
fullText(ChangeQueryBuilder.FIELD_COMMITTER).buildRepeatable(ChangeField::getCommitterParts);
+ /** The exact name, email address, and NameEmail of the committer. */
+ public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMITTER =
+ exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER)
+ .buildRepeatable(ChangeField::getCommitterNameAndEmail);
+
public static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class);
/** Serialized change object, used for pre-populating results. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index ec507f4..be4f24b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -72,8 +72,10 @@
@Deprecated static final Schema<ChangeData> V40 = schema(V39, ChangeField.PRIVATE);
@Deprecated static final Schema<ChangeData> V41 = schema(V40, ChangeField.REVIEWER_BY_EMAIL);
+ @Deprecated static final Schema<ChangeData> V42 = schema(V41, ChangeField.WIP);
- static final Schema<ChangeData> V42 = schema(V41, ChangeField.WIP);
+ static final Schema<ChangeData> V43 =
+ schema(V42, ChangeField.EXACT_AUTHOR, ChangeField.EXACT_COMMITTER);
public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
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 39e9241..af0a3b8 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
@@ -57,6 +57,7 @@
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexConfig;
import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.SchemaUtil;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -87,7 +88,9 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Function;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
@@ -127,6 +130,7 @@
public static final String FIELD_AGE = "age";
public static final String FIELD_ASSIGNEE = "assignee";
public static final String FIELD_AUTHOR = "author";
+ public static final String FIELD_EXACTAUTHOR = "exactauthor";
public static final String FIELD_BEFORE = "before";
public static final String FIELD_CHANGE = "change";
public static final String FIELD_CHANGE_ID = "change_id";
@@ -134,6 +138,7 @@
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_EXACTCOMMITTER = "exactcommitter";
public static final String FIELD_CONFLICTS = "conflicts";
public static final String FIELD_DELETED = "deleted";
public static final String FIELD_DELTA = "delta";
@@ -1119,13 +1124,21 @@
}
@Operator
- public Predicate<ChangeData> author(String who) {
- return new AuthorPredicate(who);
+ public Predicate<ChangeData> author(String who) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.EXACT_AUTHOR)) {
+ return getAuthorOrCommitterPredicate(
+ who.trim(), ExactAuthorPredicate::new, AuthorPredicate::new);
+ }
+ return getAuthorOrCommitterFullTextPredicate(who.trim(), AuthorPredicate::new);
}
@Operator
- public Predicate<ChangeData> committer(String who) {
- return new CommitterPredicate(who);
+ public Predicate<ChangeData> committer(String who) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.EXACT_COMMITTER)) {
+ return getAuthorOrCommitterPredicate(
+ who.trim(), ExactCommitterPredicate::new, CommitterPredicate::new);
+ }
+ return getAuthorOrCommitterFullTextPredicate(who.trim(), CommitterPredicate::new);
}
@Operator
@@ -1199,6 +1212,30 @@
return Predicate.or(predicates);
}
+ private Predicate<ChangeData> getAuthorOrCommitterPredicate(
+ String who,
+ Function<String, Predicate<ChangeData>> exactPredicateFunc,
+ Function<String, Predicate<ChangeData>> fullPredicateFunc)
+ throws QueryParseException {
+ if (Address.tryParse(who) != null) {
+ return exactPredicateFunc.apply(who);
+ }
+ return getAuthorOrCommitterFullTextPredicate(who, fullPredicateFunc);
+ }
+
+ private Predicate<ChangeData> getAuthorOrCommitterFullTextPredicate(
+ String who, Function<String, Predicate<ChangeData>> fullPredicateFunc)
+ throws QueryParseException {
+ Set<String> parts = SchemaUtil.getNameParts(who);
+ if (parts.isEmpty()) {
+ throw error("invalid value");
+ }
+
+ List<Predicate<ChangeData>> predicates =
+ parts.stream().map(fullPredicateFunc).collect(Collectors.toList());
+ return Predicate.and(predicates);
+ }
+
private Set<Account.Id> parseAccount(String who) throws QueryParseException, OrmException {
if (isSelf(who)) {
return Collections.singleton(self());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
new file mode 100644
index 0000000..bca5d3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2017 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.change.ChangeField.EXACT_AUTHOR;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTAUTHOR;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.Locale;
+
+public class ExactAuthorPredicate extends ChangeIndexPredicate {
+ public ExactAuthorPredicate(String value) {
+ super(EXACT_AUTHOR, FIELD_EXACTAUTHOR, value.toLowerCase(Locale.US));
+ }
+
+ @Override
+ public boolean match(ChangeData object) throws OrmException {
+ try {
+ return ChangeField.getAuthorNameAndEmail(object).contains(getValue());
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
new file mode 100644
index 0000000..3fae5e5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2017 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.change.ChangeField.EXACT_COMMITTER;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTCOMMITTER;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.Locale;
+
+public class ExactCommitterPredicate extends ChangeIndexPredicate {
+ public ExactCommitterPredicate(String value) {
+ super(EXACT_COMMITTER, FIELD_EXACTCOMMITTER, value.toLowerCase(Locale.US));
+ }
+
+ @Override
+ public boolean match(ChangeData object) throws OrmException {
+ try {
+ return ChangeField.getCommitterNameAndEmail(object).contains(getValue());
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ @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 34536ab..ba37a5e 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
@@ -124,6 +124,7 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -493,57 +494,82 @@
}
@Test
- public void byAuthor() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
-
- // 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);
-
- // Case insensitive
- assertQuery("author:jAuThOr", change1);
- assertQuery("author:ExAmPlE", change1);
-
- // By non-existing email address / name / part
- assertQuery("author:jcommitter@example.com");
- assertQuery("author:somewhere.com");
- assertQuery("author:jcommitter");
- assertQuery("author:Committer");
+ public void byAuthorExact() throws Exception {
+ byAuthorOrCommitterExact("author:");
}
@Test
- public void byCommitter() throws Exception {
+ public void byAuthorFullText() throws Exception {
+ byAuthorOrCommitterFullText("author:");
+ }
+
+ @Test
+ public void byCommitterExact() throws Exception {
+ byAuthorOrCommitterExact("committer:");
+ }
+
+ @Test
+ public void byCommitterFullText() throws Exception {
+ byAuthorOrCommitterFullText("committer:");
+ }
+
+ private void byAuthorOrCommitterExact(String searchOperator) throws Exception {
TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
+ PersonIdent john = new PersonIdent("John", "john@example.com");
+ PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
+ Change change1 = createChange(repo, johnDoe);
+ Change change2 = createChange(repo, john);
+ Change change3 = createChange(repo, doeSmith);
- // By exact email address
- assertQuery("committer:jcommitter@example.com", change1);
+ // Only email address.
+ assertQuery(searchOperator + "john.doe@example.com", change1);
+ assertQuery(searchOperator + "john@example.com", change2);
+ assertQuery(searchOperator + "Doe_SmIth@example.com", change3); // Case insensitive.
- // By email address part
- assertQuery("committer:jcommitter", change1);
- assertQuery("committer:example", change1);
- assertQuery("committer:example.com", change1);
+ // Right combination of email address and name.
+ assertQuery(searchOperator + "\"John Doe <john.doe@example.com>\"", change1);
+ assertQuery(searchOperator + "\" John <john@example.com> \"", change2);
+ assertQuery(searchOperator + "\"doE SMITH <doe_smitH@example.com>\"", change3);
- // By name part
- assertQuery("committer:Committer", change1);
+ // Wrong combination of email address and name.
+ assertQuery(searchOperator + "\"John <john.doe@example.com>\"");
+ assertQuery(searchOperator + "\"Doe John <john@example.com>\"");
+ assertQuery(searchOperator + "\"Doe John <doe_smith@example.com>\"");
+ }
- // Case insensitive
- assertQuery("committer:jCoMmItTeR", change1);
- assertQuery("committer:ExAmPlE", change1);
+ private void byAuthorOrCommitterFullText(String searchOperator) throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
+ PersonIdent john = new PersonIdent("John", "john@example.com");
+ PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
+ Change change1 = createChange(repo, johnDoe);
+ Change change2 = createChange(repo, john);
+ Change change3 = createChange(repo, doeSmith);
- // By non-existing email address / name / part
- assertQuery("committer:jauthor@example.com");
- assertQuery("committer:somewhere.com");
- assertQuery("committer:jauthor");
- assertQuery("committer:Author");
+ // By exact name.
+ assertQuery(searchOperator + "\"John Doe\"", change1);
+ assertQuery(searchOperator + "\"john\"", change2, change1);
+ assertQuery(searchOperator + "\"Doe smith\"", change3);
+
+ // By name part.
+ assertQuery(searchOperator + "Doe", change3, change1);
+ assertQuery(searchOperator + "smith", change3);
+
+ // By wrong combination.
+ assertQuery(searchOperator + "\"John Smith\"");
+
+ // By invalid query.
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("invalid value");
+ // SchemaUtil.getNameParts will return an empty set for query only containing these characters.
+ assertQuery(searchOperator + "@.- /_");
+ }
+
+ private Change createChange(TestRepository<Repo> repo, PersonIdent person) throws Exception {
+ RevCommit commit =
+ repo.parseBody(repo.commit().message("message").author(person).committer(person).create());
+ return insert(repo, newChangeForCommit(repo, commit), null);
}
@Test