Support searching changes by directories
Signed-off-by: Edwin Kempin <ekempin@google.com> Change-Id: I4192646dc50ace09e1f7b697f5b352b6ceca12fa
This commit is contained in:
@@ -306,6 +306,18 @@ 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.
|
||||
|
||||
[[footer]]
|
||||
footer:'FOOTER'::
|
||||
+
|
||||
|
||||
@@ -243,6 +243,48 @@ public class ChangeField {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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()));
|
||||
|
||||
@@ -107,7 +107,9 @@ public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
|
||||
|
||||
@Deprecated static final Schema<ChangeData> V53 = schema(V52, ChangeField.ONLY_EXTENSIONS);
|
||||
|
||||
static final Schema<ChangeData> V54 = schema(V53, ChangeField.FOOTER);
|
||||
@Deprecated static final Schema<ChangeData> V54 = schema(V53, ChangeField.FOOTER);
|
||||
|
||||
static final Schema<ChangeData> V55 = schema(V54, ChangeField.DIRECTORY);
|
||||
|
||||
public static final String NAME = "changes";
|
||||
public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
|
||||
|
||||
@@ -138,6 +138,7 @@ 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";
|
||||
@@ -772,6 +773,19 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
|
||||
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)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1502,6 +1502,81 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void byComment() throws Exception {
|
||||
TestRepository<Repo> repo = createProject("repo");
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
'conflicts:',
|
||||
'deleted:',
|
||||
'delta:',
|
||||
'dir:',
|
||||
'directory:',
|
||||
'ext:',
|
||||
'extension:',
|
||||
'file:',
|
||||
|
||||
Reference in New Issue
Block a user