Support searching changes by directories

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I4192646dc50ace09e1f7b697f5b352b6ceca12fa
This commit is contained in:
Edwin Kempin
2019-01-25 15:20:52 +01:00
parent f148c79933
commit 0e88d5d69a
7 changed files with 188 additions and 1 deletions

View File

@@ -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'::
+

View File

@@ -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()));

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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");

View File

@@ -36,6 +36,8 @@
'conflicts:',
'deleted:',
'delta:',
'dir:',
'directory:',
'ext:',
'extension:',
'file:',