From 0e88d5d69a7f2b0d470daa7b4e7b34d3d0c96e99 Mon Sep 17 00:00:00 2001 From: Edwin Kempin Date: Fri, 25 Jan 2019 15:20:52 +0100 Subject: [PATCH] Support searching changes by directories Signed-off-by: Edwin Kempin Change-Id: I4192646dc50ace09e1f7b697f5b352b6ceca12fa --- Documentation/user-search.txt | 12 +++ .../server/index/change/ChangeField.java | 42 +++++++++++ .../index/change/ChangeSchemaDefinitions.java | 4 +- .../query/change/ChangeQueryBuilder.java | 14 ++++ .../query/change/DirectoryPredicate.java | 40 ++++++++++ .../change/AbstractQueryChangesTest.java | 75 +++++++++++++++++++ .../core/gr-search-bar/gr-search-bar.js | 2 + 7 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 java/com/google/gerrit/server/query/change/DirectoryPredicate.java diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt index 2016de70d6..709491e33c 100644 --- a/Documentation/user-search.txt +++ b/Documentation/user-search.txt @@ -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':: + diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java index 6e5a064f58..bcfcff1633 100644 --- a/java/com/google/gerrit/server/index/change/ChangeField.java +++ b/java/com/google/gerrit/server/index/change/ChangeField.java @@ -243,6 +243,48 @@ public class ChangeField { } } + /** Folders that are touched by the current patch set. */ + public static final FieldDef> DIRECTORY = + exact(ChangeQueryBuilder.FIELD_DIRECTORY).buildRepeatable(ChangeField::getDirectories); + + public static Set getDirectories(ChangeData cd) throws OrmException { + List paths; + try { + paths = cd.currentFilePaths(); + } catch (IOException e) { + throw new OrmException(e); + } + + Splitter s = Splitter.on('/').omitEmptyStrings(); + Set 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 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 77e9ff478e..dff95ce502 100644 --- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java +++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java @@ -107,7 +107,9 @@ public class ChangeSchemaDefinitions extends SchemaDefinitions { @Deprecated static final Schema V53 = schema(V52, ChangeField.ONLY_EXTENSIONS); - static final Schema V54 = schema(V53, ChangeField.FOOTER); + @Deprecated static final Schema V54 = schema(V53, ChangeField.FOOTER); + + static final Schema V55 = schema(V54, ChangeField.DIRECTORY); 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 153ad308f8..33e1575767 100644 --- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java +++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java @@ -138,6 +138,7 @@ public class ChangeQueryBuilder extends QueryBuilder { 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 { throw new QueryParseException("'footer' operator is not supported by change index version"); } + @Operator + public Predicate dir(String directory) throws QueryParseException { + return directory(directory); + } + + @Operator + public Predicate 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 label(String name) throws QueryParseException, OrmException, IOException, ConfigInvalidException { 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 0000000000..676a2089c1 --- /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/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java index 26e9d694e3..44c24da498 100644 --- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java +++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java @@ -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 = 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 = createProject("repo"); 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 5b590df8ce..6699bd1a1a 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,6 +36,8 @@ 'conflicts:', 'deleted:', 'delta:', + 'dir:', + 'directory:', 'ext:', 'extension:', 'file:',