Merge changes I8865dbf9,I42774ce8,I4192646d,Id7e08e57
* changes: Support searching changes that touch directories by regular expressions Allow matching changes with files that have no extension by 'ext' operator Support searching changes by directories Support searching changes by commit message footer
This commit is contained in:
@@ -296,8 +296,8 @@ extension:'EXT', ext:'EXT'::
|
||||
+
|
||||
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'::
|
||||
@@ -308,6 +308,29 @@ 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'::
|
||||
+
|
||||
|
||||
@@ -192,7 +192,7 @@ public class ChangeField {
|
||||
exact(ChangeQueryBuilder.FIELD_EXTENSION).buildRepeatable(ChangeField::getExtensions);
|
||||
|
||||
public static Set<String> getExtensions(ChangeData cd) throws OrmException {
|
||||
return extensions(cd).filter(e -> !e.isEmpty()).collect(toSet());
|
||||
return extensions(cd).collect(toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,6 +228,63 @@ public class ChangeField {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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()));
|
||||
|
||||
@@ -105,7 +105,14 @@ public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
|
||||
|
||||
@Deprecated static final Schema<ChangeData> V52 = schema(V51, ChangeField.EXTENSION);
|
||||
|
||||
static final Schema<ChangeData> V53 = schema(V52, ChangeField.ONLY_EXTENSIONS);
|
||||
@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();
|
||||
|
||||
@@ -138,9 +138,11 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
|
||||
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";
|
||||
@@ -763,6 +765,31 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
|
||||
"'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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1379,7 +1379,7 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
|
||||
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,12 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
|
||||
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
|
||||
@@ -1445,6 +1451,143 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
|
||||
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
|
||||
public void byComment() throws Exception {
|
||||
TestRepository<Repo> repo = createProject("repo");
|
||||
|
||||
@@ -36,9 +36,12 @@
|
||||
'conflicts:',
|
||||
'deleted:',
|
||||
'delta:',
|
||||
'dir:',
|
||||
'directory:',
|
||||
'ext:',
|
||||
'extension:',
|
||||
'file:',
|
||||
'footer:',
|
||||
'from:',
|
||||
'has:',
|
||||
'has:draft',
|
||||
|
||||
Reference in New Issue
Block a user