Merge changes I8359a0fe,Id4dbe193
* changes:
MailUtil: Suppress deprecation warning for usage of AccountResolver#resolveByNameOrEmail
ReviewerAdder: Remove redundant null checks
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index abd2531..5d7a78b 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -276,6 +276,8 @@
ones using a bracket expression). For example, to match all XML
files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
`file:"^name[1-3].xml"`.
++
+Slash ('/') is used path separator.
[[file]]
file:'NAME', f:'NAME'::
@@ -294,8 +296,40 @@
+
Matches any change touching a file with extension 'EXT', case-insensitive. The
extension is defined as the portion of the filename following the final `.`.
-Files with no `.` in their name have no extension and cannot be matched with
-this operator; use `file:` instead.
+Files with no `.` in their name have no extension and can be matched by an
+empty string.
+
+[[onlyextensions]]
+onlyextensions:'EXT_LIST', onlyexts:'EXT_LIST'::
++
+Matches any change touching only files with extensions that are listed in
+'EXT_LIST' (comma-separated list). The matching is done case-insensitive.
+An extension is defined as the portion of the filename following the final `.`.
+Files with no `.` in their name have no extension and can be matched by an
+empty string.
+
+[[directory]]
+directory:'DIR', dir:'DIR'::
++
+Matches any change where the current patch set touches a file in the directory
+'DIR'. The matching is done case-insensitive. 'DIR' can be a full directory
+name, a directory prefix or any combination of intermediate directory segments.
+E.g. a change that touches a file in the directory 'a/b/c' matches for 'a/b/c',
+'a', 'a/b', 'b', 'b/c' and 'c'.
++
+Slash ('/') is used path separator. Leading and trailing slashes are allowed
+but are not mandatory.
++
+If 'DIR' starts with `^` it matches directories and directory segments by
+regular expression. The link:http://www.brics.dk/automaton/[dk.brics.automaton
+library] is used for evaluation of such patterns.
+
+[[footer]]
+footer:'FOOTER'::
++
+Matches any change that has 'FOOTER' as footer in the commit message of the
+current patch set. 'FOOTER' can be specified verbatim ('<key>: <value>', must
+be quoted) or as '<key>=<value>'. The matching is done case-insensitive.
[[star]]
star:'LABEL'::
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 52dac9d..593fb85 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -24,6 +24,7 @@
import static com.google.gerrit.index.FieldDef.storedOnly;
import static com.google.gerrit.index.FieldDef.timestamp;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -191,6 +192,29 @@
exact(ChangeQueryBuilder.FIELD_EXTENSION).buildRepeatable(ChangeField::getExtensions);
public static Set<String> getExtensions(ChangeData cd) throws OrmException {
+ return extensions(cd).collect(toSet());
+ }
+
+ /**
+ * File extensions of each file modified in the current patch set as a sorted list. The purpose of
+ * this field is to allow matching changes that only touch files with certain file extensions.
+ */
+ public static final FieldDef<ChangeData, String> ONLY_EXTENSIONS =
+ exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS).build(ChangeField::getAllExtensionsAsList);
+
+ public static String getAllExtensionsAsList(ChangeData cd) throws OrmException {
+ return extensions(cd).distinct().sorted().collect(joining(","));
+ }
+
+ /**
+ * Returns a stream with all file extensions that are used by files in the given change. A file
+ * extension is defined as the portion of the filename following the final `.`. Files with no `.`
+ * in their name have no extension. For them an empty string is returned as part of the stream.
+ *
+ * <p>If the change contains multiple files with the same extension the extension is returned
+ * multiple times in the stream (once per file).
+ */
+ private static Stream<String> extensions(ChangeData cd) throws OrmException {
try {
return cd.currentFilePaths()
.stream()
@@ -198,14 +222,69 @@
// If we want to find "all Java files", we want to match both .java and .JAVA, even if we
// normally care about case sensitivity. (Whether we should change the existing file/path
// predicates to be case insensitive is a separate question.)
- .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US))
- .filter(e -> !e.isEmpty())
+ .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US));
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ /** Footers from the commit message of the current patch set. */
+ public static final FieldDef<ChangeData, Iterable<String>> FOOTER =
+ exact(ChangeQueryBuilder.FIELD_FOOTER).buildRepeatable(ChangeField::getFooters);
+
+ public static Set<String> getFooters(ChangeData cd) throws OrmException {
+ try {
+ return cd.commitFooters()
+ .stream()
+ .map(f -> f.toString().toLowerCase(Locale.US))
.collect(toSet());
} catch (IOException e) {
throw new OrmException(e);
}
}
+ /** Folders that are touched by the current patch set. */
+ public static final FieldDef<ChangeData, Iterable<String>> DIRECTORY =
+ exact(ChangeQueryBuilder.FIELD_DIRECTORY).buildRepeatable(ChangeField::getDirectories);
+
+ public static Set<String> getDirectories(ChangeData cd) throws OrmException {
+ List<String> paths;
+ try {
+ paths = cd.currentFilePaths();
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+
+ Splitter s = Splitter.on('/').omitEmptyStrings();
+ Set<String> r = new HashSet<>();
+ for (String path : paths) {
+ StringBuilder directory = new StringBuilder();
+ directory.append("");
+ r.add(directory.toString());
+ String nextPart = null;
+ for (String part : s.split(path)) {
+ if (nextPart != null) {
+ r.add(nextPart);
+
+ if (directory.length() > 0) {
+ directory.append("/");
+ }
+ directory.append(nextPart);
+
+ String intermediateDir = directory.toString();
+ int i = intermediateDir.indexOf('/');
+ while (i >= 0) {
+ r.add(intermediateDir);
+ intermediateDir = intermediateDir.substring(i + 1);
+ i = intermediateDir.indexOf('/');
+ }
+ }
+ nextPart = part;
+ }
+ }
+ return r;
+ }
+
/** Owner/creator of the change. */
public static final FieldDef<ChangeData, Integer> OWNER =
integer(ChangeQueryBuilder.FIELD_OWNER).build(changeGetter(c -> c.getOwner().get()));
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index cd24c92..cde6a64 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -103,7 +103,16 @@
@Deprecated static final Schema<ChangeData> V51 = schema(V50, ChangeField.TOTAL_COMMENT_COUNT);
- static final Schema<ChangeData> V52 = schema(V51, ChangeField.EXTENSION);
+ @Deprecated static final Schema<ChangeData> V52 = schema(V51, ChangeField.EXTENSION);
+
+ @Deprecated static final Schema<ChangeData> V53 = schema(V52, ChangeField.ONLY_EXTENSIONS);
+
+ @Deprecated static final Schema<ChangeData> V54 = schema(V53, ChangeField.FOOTER);
+
+ @Deprecated static final Schema<ChangeData> V55 = schema(V54, ChangeField.DIRECTORY);
+
+ // The computation of the 'extension' field is changed, hence reindexing is required.
+ static final Schema<ChangeData> V56 = schema(V55);
public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 0b889dc..b62a1d7 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -138,8 +138,11 @@
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_DIRECTORY = "directory";
public static final String FIELD_EXACTCOMMITTER = "exactcommitter";
public static final String FIELD_EXTENSION = "extension";
+ public static final String FIELD_ONLY_EXTENSIONS = "onlyextensions";
+ public static final String FIELD_FOOTER = "footer";
public static final String FIELD_CONFLICTS = "conflicts";
public static final String FIELD_DELETED = "deleted";
public static final String FIELD_DELTA = "delta";
@@ -749,6 +752,45 @@
}
@Operator
+ public Predicate<ChangeData> onlyexts(String extList) throws QueryParseException {
+ return onlyextensions(extList);
+ }
+
+ @Operator
+ public Predicate<ChangeData> onlyextensions(String extList) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.ONLY_EXTENSIONS)) {
+ return new FileExtensionListPredicate(extList);
+ }
+ throw new QueryParseException(
+ "'onlyextensions' operator is not supported by change index version");
+ }
+
+ @Operator
+ public Predicate<ChangeData> footer(String footer) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.FOOTER)) {
+ return new FooterPredicate(footer);
+ }
+ throw new QueryParseException("'footer' operator is not supported by change index version");
+ }
+
+ @Operator
+ public Predicate<ChangeData> dir(String directory) throws QueryParseException {
+ return directory(directory);
+ }
+
+ @Operator
+ public Predicate<ChangeData> directory(String directory) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.DIRECTORY)) {
+ if (directory.startsWith("^")) {
+ return new RegexDirectoryPredicate(directory);
+ }
+
+ return new DirectoryPredicate(directory);
+ }
+ throw new QueryParseException("'directory' operator is not supported by change index version");
+ }
+
+ @Operator
public Predicate<ChangeData> label(String name)
throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Set<Account.Id> accounts = null;
diff --git a/java/com/google/gerrit/server/query/change/DirectoryPredicate.java b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
new file mode 100644
index 0000000..676a208
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 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.common.base.CharMatcher;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import java.util.Locale;
+
+public class DirectoryPredicate extends ChangeIndexPredicate {
+ private static String clean(String directory) {
+ return CharMatcher.is('/').trimFrom(directory).toLowerCase(Locale.US);
+ }
+
+ DirectoryPredicate(String value) {
+ super(ChangeField.DIRECTORY, clean(value));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return ChangeField.getDirectories(cd).contains(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
new file mode 100644
index 0000000..3399338
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2019 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 java.util.stream.Collectors.joining;
+
+import com.google.common.base.Splitter;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+
+public class FileExtensionListPredicate extends ChangeIndexPredicate {
+ private static String clean(String extList) {
+ return Splitter.on(',')
+ .splitToList(extList)
+ .stream()
+ .map(FileExtensionPredicate::clean)
+ .distinct()
+ .sorted()
+ .collect(joining(","));
+ }
+
+ FileExtensionListPredicate(String value) {
+ super(ChangeField.ONLY_EXTENSIONS, clean(value));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return ChangeField.getAllExtensionsAsList(cd).equals(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
index ee5030a..5353f11 100644
--- a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
@@ -19,7 +19,7 @@
import java.util.Locale;
public class FileExtensionPredicate extends ChangeIndexPredicate {
- private static String clean(String ext) {
+ static String clean(String ext) {
if (ext.startsWith(".")) {
ext = ext.substring(1);
}
diff --git a/java/com/google/gerrit/server/query/change/FooterPredicate.java b/java/com/google/gerrit/server/query/change/FooterPredicate.java
new file mode 100644
index 0000000..1d7d19b
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FooterPredicate.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2019 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.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import java.util.Locale;
+
+public class FooterPredicate extends ChangeIndexPredicate {
+ private static String clean(String value) {
+ int indexEquals = value.indexOf('=');
+ int indexColon = value.indexOf(':');
+
+ // footer key cannot contain '='
+ if (indexEquals > 0 && (indexEquals < indexColon || indexColon < 0)) {
+ value = value.substring(0, indexEquals) + ": " + value.substring(indexEquals + 1);
+ }
+ return value.toLowerCase(Locale.US);
+ }
+
+ FooterPredicate(String value) {
+ super(ChangeField.FOOTER, clean(value));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return ChangeField.getFooters(cd).contains(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
new file mode 100644
index 0000000..1d49f1e
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2019 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.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+public class RegexDirectoryPredicate extends ChangeRegexPredicate {
+ protected final RunAutomaton pattern;
+
+ public RegexDirectoryPredicate(String re) {
+ super(ChangeField.DIRECTORY, re);
+
+ if (re.startsWith("^")) {
+ re = re.substring(1);
+ }
+
+ if (re.endsWith("$") && !re.endsWith("\\$")) {
+ re = re.substring(0, re.length() - 1);
+ }
+
+ this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return ChangeField.getDirectories(cd).stream().anyMatch(pattern::run);
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 79eae2a..7872a82 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1379,7 +1379,7 @@
Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc"));
Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC"));
Change change3 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
- Change change4 = insert(repo, newChangeWithFiles(repo, "Quux.java"));
+ Change change4 = insert(repo, newChangeWithFiles(repo, "Quux.java", "foo"));
assertQuery("extension:java", change4);
assertQuery("ext:java", change4);
@@ -1387,6 +1387,205 @@
assertQuery("ext:jAvA", change4);
assertQuery("ext:.jAvA", change4);
assertQuery("ext:cc", change3, change2, change1);
+
+ if (getSchemaVersion() >= 56) {
+ // matching changes with files that have no extension is possible
+ assertQuery("ext:\"\"", change4);
+ assertFailingQuery("ext:");
+ }
+ }
+
+ @Test
+ public void byOnlyExtensions() throws Exception {
+ if (getSchemaVersion() < 53) {
+ assertMissingField(ChangeField.ONLY_EXTENSIONS);
+ String unsupportedOperatorMessage =
+ "'onlyextensions' operator is not supported by change index version";
+ assertFailingQuery("onlyextensions:txt,jpg", unsupportedOperatorMessage);
+ assertFailingQuery("onlyexts:txt,jpg", unsupportedOperatorMessage);
+ return;
+ }
+
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
+ Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
+ Change change3 = insert(repo, newChangeWithFiles(repo, "foo.CC", "bar.cc"));
+ Change change4 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+ Change change5 = insert(repo, newChangeWithFiles(repo, "Quux.java"));
+ Change change6 = insert(repo, newChangeWithFiles(repo, "foo.txt", "foo"));
+ Change change7 = insert(repo, newChangeWithFiles(repo, "foo"));
+
+ // case doesn't matter
+ assertQuery("onlyextensions:cc,h", change4, change2, change1);
+ assertQuery("onlyextensions:CC,H", change4, change2, change1);
+ assertQuery("onlyextensions:cc,H", change4, change2, change1);
+ assertQuery("onlyextensions:cC,h", change4, change2, change1);
+ assertQuery("onlyextensions:cc", change3);
+ assertQuery("onlyextensions:CC", change3);
+ assertQuery("onlyexts:java", change5);
+ assertQuery("onlyexts:jAvA", change5);
+ assertQuery("onlyexts:.jAvA", change5);
+
+ // order doesn't matter
+ assertQuery("onlyextensions:h,cc", change4, change2, change1);
+ assertQuery("onlyextensions:H,CC", change4, change2, change1);
+
+ // specifying extension with '.' is okay
+ assertQuery("onlyextensions:.cc,.h", change4, change2, change1);
+ assertQuery("onlyextensions:cc,.h", change4, change2, change1);
+ assertQuery("onlyextensions:.cc,h", change4, change2, change1);
+ assertQuery("onlyexts:.java", change5);
+
+ // matching changes without extension is possible
+ assertQuery("onlyexts:txt");
+ assertQuery("onlyexts:txt,", change6);
+ assertQuery("onlyexts:,txt", change6);
+ assertQuery("onlyextensions:\"\"", change7);
+ assertQuery("onlyexts:\"\"", change7);
+ assertQuery("onlyextensions:,", change7);
+ assertQuery("onlyexts:,", change7);
+ assertFailingQuery("onlyextensions:");
+ assertFailingQuery("onlyexts:");
+
+ // inverse queries
+ assertQuery("-onlyextensions:cc,h", change7, change6, change5, change3);
+ }
+
+ @Test
+ public void byFooter() throws Exception {
+ if (getSchemaVersion() < 54) {
+ assertMissingField(ChangeField.FOOTER);
+ assertFailingQuery(
+ "footer:Change-Id=I3d2b978ed455f835d1dad2daa920be0b0ec2ae36",
+ "'footer' operator is not supported by change index version");
+ return;
+ }
+
+ TestRepository<Repo> repo = createProject("repo");
+ RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
+ Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nfoo: baz").create());
+ Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ RevCommit commit3 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar\nfoo:baz").create());
+ Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar=baz").create());
+ Change change4 = insert(repo, newChangeForCommit(repo, commit4));
+
+ // create a changes with lines that look like footers, but which are not
+ RevCommit commit5 =
+ repo.parseBody(
+ repo.commit().message("Test\n\nfoo: bar\n\nfoo=bar").insertChangeId().create());
+ Change change5 = insert(repo, newChangeForCommit(repo, commit5));
+ RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
+ insert(repo, newChangeForCommit(repo, commit6));
+
+ // matching by 'key=value' works
+ assertQuery("footer:foo=bar", change3, change1);
+ assertQuery("footer:foo=baz", change3, change2);
+ assertQuery("footer:Change-Id=" + change5.getKey(), change5);
+ assertQuery("footer:foo=bar=baz", change4);
+
+ // case doesn't matter
+ assertQuery("footer:foo=BAR", change3, change1);
+ assertQuery("footer:FOO=bar", change3, change1);
+ assertQuery("footer:fOo=BaZ", change3, change2);
+
+ // verbatim matching of footers works
+ assertQuery("footer:\"foo: bar\"", change3, change1);
+ assertQuery("footer:\"foo: baz\"", change3, change2);
+ assertQuery("footer:\"Change-Id: " + change5.getKey() + "\"", change5);
+ assertQuery("footer:\"foo: bar=baz\"", change4);
+
+ // expect no match because 'a=b: c' of commit6 is not a valid footer (footer key cannot contain
+ // '=')
+ assertQuery("footer:a=b=c");
+ assertQuery("footer:\"a=b: c\"");
+
+ // expect empty result for invalid footers
+ assertQuery("footer:foo");
+ assertQuery("footer:foo=");
+ assertQuery("footer:=foo");
+ assertQuery("footer:=");
+ }
+
+ @Test
+ public void byDirectory() throws Exception {
+ if (getSchemaVersion() < 55) {
+ assertMissingField(ChangeField.DIRECTORY);
+ String unsupportedOperatorMessage =
+ "'directory' operator is not supported by change index version";
+ assertFailingQuery("directory:src/java", unsupportedOperatorMessage);
+ assertFailingQuery("dir:src/java", unsupportedOperatorMessage);
+ return;
+ }
+
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
+ Change change2 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ Change change3 =
+ insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+ Change change4 = insert(repo, newChangeWithFiles(repo, "a.txt"));
+ Change change5 = insert(repo, newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
+
+ // matching by directory prefix works
+ assertQuery("directory:src", change2, change1);
+ assertQuery("directory:src/java", change2);
+ assertQuery("directory:src/js", change2);
+ assertQuery("directory:documentation/", change3);
+ assertQuery("directory:documentation/training", change3);
+ assertQuery("directory:documentation/training/slides", change3);
+
+ // 'dir' alias works
+ assertQuery("dir:src", change2, change1);
+ assertQuery("dir:src/java", change2);
+
+ // case doesn't matter
+ assertQuery("directory:Documentation/TrAiNiNg/SLIDES", change3);
+
+ // leading and trailing '/' doesn't matter
+ assertQuery("directory:/documentation/training/slides", change3);
+ assertQuery("directory:documentation/training/slides/", change3);
+ assertQuery("directory:/documentation/training/slides/", change3);
+
+ // files do not match as directory
+ assertQuery("directory:src/foo.h");
+ assertQuery("directory:documentation/training/slides/README.txt");
+
+ // root directory matches all changes
+ assertQuery("directory:/", change5, change4, change3, change2, change1);
+ assertQuery("directory:\"\"", change5, change4, change3, change2, change1);
+ assertFailingQuery("directory:");
+
+ // matching single directory segments works
+ assertQuery("directory:java", change2);
+ assertQuery("directory:slides", change3);
+
+ // files do not match as directory segment
+ assertQuery("directory:foo.h");
+
+ // matching any combination of intermediate directory segments works
+ assertQuery("directory:training/slides", change3);
+ assertQuery("directory:b/c", change5);
+ assertQuery("directory:b/c/d", change5);
+ assertQuery("directory:b/c/d/e", change5);
+ assertQuery("directory:c/d", change5);
+ assertQuery("directory:c/d/e", change5);
+ assertQuery("directory:d/e", change5);
+
+ // files do not match as directory segments
+ assertQuery("directory:d/e/foo.txt");
+ assertQuery("directory:e/foo.txt");
+
+ // matching any combination of intermediate directory segments works with leading and trailing
+ // '/'
+ assertQuery("directory:/b/c", change5);
+ assertQuery("directory:/b/c/", change5);
+ assertQuery("directory:b/c/", change5);
+
+ // match by regexp
+ assertQuery("directory:^.*va.*", change2);
+ assertQuery("directory:^documentation/.*/slides", change3);
+ assertQuery("directory:^train.*", change3);
}
@Test
@@ -3126,12 +3325,19 @@
.isFalse();
}
- protected void assertFailingQuery(String query, String expectedMessage) throws Exception {
+ protected void assertFailingQuery(String query) throws Exception {
+ assertFailingQuery(query, null);
+ }
+
+ protected void assertFailingQuery(String query, @Nullable String expectedMessage)
+ throws Exception {
try {
assertQuery(query);
fail("expected BadRequestException for query '" + query + "'");
} catch (BadRequestException e) {
- assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ if (expectedMessage != null) {
+ assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ }
}
}
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 8aff87e..6699bd1 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -36,9 +36,12 @@
'conflicts:',
'deleted:',
'delta:',
+ 'dir:',
+ 'directory:',
'ext:',
'extension:',
'file:',
+ 'footer:',
'from:',
'has:',
'has:draft',
@@ -66,6 +69,8 @@
'is:wip',
'label:',
'message:',
+ 'onlyexts:',
+ 'onlyextensions:',
'owner:',
'ownerin:',
'parentproject:',