FindOwnersBackend: Fix incompatibility of handling globs
The find-owners backend promises to be compatible with the syntax of the
find-owners plugin, but currently the handling of globs differs.
The find-owners plugin ignores subdirectories when checking if a glob
matches a file.
Examples:
1. the glob "*.md" in "/" matches "foo.md", "bar/foo.md" and
"baz/bar/foo.md"
2. the glob "BUILD" in "/" matches "BUILD", "bar/BUILD" and
"baz/bar/BUILD"
3. the glob "foo-[1-4].txt" in "/" matches "foo-1.txt", "bar/foo-1.txt"
and "baz/bar/foo-1.txt"
This is because the find-owners plugin iterates over all parent
directories [1,2] to see if there is a glob that matches the
filename [3]. E.g. if the glob "*.md" is defined at '/' and the file
path is "baz/bar/foo.md" then it does the following checks:
a. Is there a glob at "./baz/bar" that matches file "./baz/bar/foo.md" (no)
b. Is there a glob at "./baz" that matches file "./baz/foo.md" (no)
c. Is there a glob at "./" that matches file "./foo.md" (yes)
Effectively this means that subdirectories are ignored (although the
actual file is "./baz/bar/foo.md" the find-owners plugin checks the glob
against "./foo.md").
To be compatible with this behaviour the code-owners plugin needs to
ignore subdirectories too when checking if a file matches a glob.
Instead of iterating over the parent directories for each file as it's
done in the find-owners plugin, we simply prefix all globs with "{**/,}"
("{**/,}" matches "**/" or "", which matches any subdirectory and no
subdirectory).
Change I1ddc8bf32 was a first attempt to fix this by replacing "*" with
"**" in globs, but this only fixed case 1 (and not the cases 2 and 3).
[1] https://gerrit.googlesource.com/plugins/find-owners/+/refs/heads/master/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java#367
[2] https://gerrit.googlesource.com/plugins/find-owners/+/refs/heads/master/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java#396
[3] https://gerrit.googlesource.com/plugins/find-owners/+/refs/heads/master/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java#375
Bug: Issue 14008
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I04931606858511fb1c36c449a4ce3f2300eabecf
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcher.java b/java/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcher.java
index 2db7b6d..650ae50 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcher.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcher.java
@@ -14,8 +14,6 @@
package com.google.gerrit.plugins.codeowners.backend;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.flogger.FluentLogger;
import java.nio.file.Path;
/**
@@ -28,8 +26,6 @@
* </ul>
*/
public class FindOwnersGlobMatcher implements PathExpressionMatcher {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
/** Singleton instance. */
public static FindOwnersGlobMatcher INSTANCE = new FindOwnersGlobMatcher();
@@ -38,43 +34,7 @@
@Override
public boolean matches(String glob, Path relativePath) {
- String adaptedGlob = replaceSingleStarWithDoubleStar(glob);
- logger.atFine().log("adapted glob = %s", adaptedGlob);
- return GlobMatcher.INSTANCE.matches(adaptedGlob, relativePath);
- }
-
- /**
- * Replaces any single '*' in the given glob with '**'. Non-single '*'s, like '**' or '***', stay
- * unchanged.
- *
- * @param glob glob in which any single '*' should be replaced by '**'
- */
- @VisibleForTesting
- String replaceSingleStarWithDoubleStar(String glob) {
- StringBuilder adaptedGlob = new StringBuilder();
- Character previousChar = null;
- boolean maybeSingleStar = false;
- for (char nextCharacter : glob.toCharArray()) {
- if (maybeSingleStar && nextCharacter != '*') {
- // the previous character was a '*' that was not preceded by '*' (maybeSingleStar == true),
- // since the next character is not '*', we are now sure that the previous character was a
- // single '*' which should be replaced by '**',
- // to do this append another '*'
- adaptedGlob.append('*');
- }
- adaptedGlob.append(nextCharacter);
-
- // the current character may be a single '*' if it's not preceded by '*'
- maybeSingleStar =
- nextCharacter == '*' && (previousChar == null || previousChar.charValue() != '*');
- previousChar = nextCharacter;
- }
-
- if (maybeSingleStar) {
- // the last character was a '*' that was not preceded by '*'
- adaptedGlob.append('*');
- }
-
- return adaptedGlob.toString();
+ // always match files in all subdirectories
+ return GlobMatcher.INSTANCE.matches("{**/,}" + glob, relativePath);
}
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcherTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcherTest.java
index 7e0c03e..8696f19 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcherTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/FindOwnersGlobMatcherTest.java
@@ -14,8 +14,6 @@
package com.google.gerrit.plugins.codeowners.backend;
-import static com.google.common.truth.Truth.assertThat;
-
import org.junit.Test;
/** Tests for {@link FindOwnersGlobMatcher}. */
@@ -25,67 +23,68 @@
return FindOwnersGlobMatcher.INSTANCE;
}
- @Test
- public void singleStarInGlobIsReplacedWithDoubleStar() throws Exception {
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("*.md"))
- .isEqualTo("**.md");
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("foo/*.md"))
- .isEqualTo("foo/**.md");
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("*/foo/*.md"))
- .isEqualTo("**/foo/**.md");
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("foo/*"))
- .isEqualTo("foo/**");
- }
-
- @Test
- public void doubleStarInGlobIsNotReplaced() throws Exception {
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("**.md"))
- .isEqualTo("**.md");
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("foo/**.md"))
- .isEqualTo("foo/**.md");
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("**/foo/**.md"))
- .isEqualTo("**/foo/**.md");
- }
-
- @Test
- public void tripleStarInGlobIsNotReplaced() throws Exception {
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("***.md"))
- .isEqualTo("***.md");
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("foo/***.md"))
- .isEqualTo("foo/***.md");
- assertThat(FindOwnersGlobMatcher.INSTANCE.replaceSingleStarWithDoubleStar("***/foo/***.md"))
- .isEqualTo("***/foo/***.md");
- }
-
/**
- * This test differs from the base class ({@link GlobMatcherTest}), since for {@link
- * FindOwnersGlobMatcher} {@code *} also matches slashes and the test in the base class has an
- * assertion that checks that slashes are not matched by {@code *}.
+ * This test differs from the base class ({@link GlobMatcherTest}), since {@link
+ * FindOwnersGlobMatcher} matches globs against any subdirectory and the test in the base class
+ * checks that subdirectories are not matched.
*/
@Test
@Override
- public void matchFileTypeInCurrentFolder() throws Exception {
- String pathExpression = "*.md";
- assertMatch(pathExpression, "README.md", "config.md");
- assertNoMatch(pathExpression, "README", "README.md5");
- }
-
- @Test
- public void matchFileTypeInCurrentFolderAndAllSubfoldersBySingleStar() throws Exception {
+ public void matchFileType() throws Exception {
String pathExpression = "*.md";
assertMatch(pathExpression, "README.md", "config.md", "foo/README.md", "foo/bar/README.md");
assertNoMatch(pathExpression, "README", "README.md5");
}
+ /**
+ * This test differs from the base class ({@link GlobMatcherTest}), since {@link
+ * FindOwnersGlobMatcher} matches globs against any subdirectory and the test in the base class
+ * checks that subdirectories are not matched.
+ */
@Test
- public void matchAllFilesInSubfolderBySingleStar() throws Exception {
- String pathExpression = "foo/*";
+ @Override
+ public void matchConcreteFile() throws Exception {
+ String pathExpression = "BUILD";
+ assertMatch(pathExpression, "BUILD", "foo/BUILD", "foo/bar/BUILD");
+ assertNoMatch(pathExpression, "README", "BUILD2");
+ }
+
+ /**
+ * This test differs from the base class ({@link GlobMatcherTest}), since {@link
+ * FindOwnersGlobMatcher} matches globs against any subdirectory and the test in the base class
+ * checks that subdirectories are not matched.
+ */
+ @Test
+ @Override
+ public void matchAllFilesInSubfolder() throws Exception {
+ String pathExpression = "foo/**";
assertMatch(
pathExpression,
"foo/README.md",
"foo/config.txt",
"foo/bar/README.md",
- "foo/bar/baz/README.md");
- assertNoMatch(pathExpression, "README", "foo2/README");
+ "foo/bar/baz/README.md",
+ "bar/foo/README.md",
+ "bar/foo/config.txt",
+ "bar/foo/bar/README.md",
+ "bar/foo/bar/baz/README.md");
+ assertNoMatch(pathExpression, "README", "foo2/README", "bar/README", "bar/foo2/README");
+ }
+
+ /**
+ * This test differs from the base class ({@link GlobMatcherTest}), since {@link
+ * FindOwnersGlobMatcher} matches globs against any subdirectory and the test in the base class
+ * checks that subdirectories are not matched.
+ */
+ @Test
+ @Override
+ public void matchPattern() throws Exception {
+ String pathExpression = "{**/,}foo-[1-4].txt";
+ assertMatch(pathExpression, "foo-1.txt", "foo-2.txt", "sub/foo-3.txt", "sub/sub/foo-4.txt");
+ assertNoMatch(pathExpression, "foo-5.txt", "foo-11.txt");
+
+ String pathExpression2 = "foo-[1-4].txt";
+ assertMatch(pathExpression2, "foo-1.txt", "foo-2.txt", "sub/foo-3.txt", "sub/sub/foo-4.txt");
+ assertNoMatch(pathExpression2, "foo-5.txt", "foo-11.txt");
}
}
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/GlobMatcherTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/GlobMatcherTest.java
index 75affd9..5291bb7 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/GlobMatcherTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/GlobMatcherTest.java
@@ -24,17 +24,17 @@
}
@Test
- public void matchConcreteFileInCurrentFolder() throws Exception {
+ public void matchConcreteFile() throws Exception {
String pathExpression = "BUILD";
assertMatch(pathExpression, "BUILD");
- assertNoMatch(pathExpression, "README", "BUILD2", "foo/BUILD");
+ assertNoMatch(pathExpression, "README", "BUILD2", "foo/BUILD", "foo/bar/BUILD");
}
@Test
- public void matchFileTypeInCurrentFolder() throws Exception {
+ public void matchFileType() throws Exception {
String pathExpression = "*.md";
assertMatch(pathExpression, "README.md", "config.md");
- assertNoMatch(pathExpression, "README", "README.md5", "foo/README.md");
+ assertNoMatch(pathExpression, "README", "README.md5", "foo/README.md", "foo/bar/README.md");
}
@Test
@@ -60,7 +60,16 @@
"foo/config.txt",
"foo/bar/README.md",
"foo/bar/baz/README.md");
- assertNoMatch(pathExpression, "README", "foo2/README");
+ assertNoMatch(
+ pathExpression,
+ "README",
+ "foo2/README",
+ "bar/README",
+ "bar/foo2/README",
+ "bar/foo/README.md",
+ "bar/foo/config.txt",
+ "bar/foo/bar/README.md",
+ "bar/foo/bar/baz/README.md");
}
@Test
@@ -68,5 +77,9 @@
String pathExpression = "{**/,}foo-[1-4].txt";
assertMatch(pathExpression, "foo-1.txt", "foo-2.txt", "sub/foo-3.txt", "sub/sub/foo-4.txt");
assertNoMatch(pathExpression, "foo-5.txt", "foo-11.txt");
+
+ String pathExpression2 = "foo-[1-4].txt";
+ assertMatch(pathExpression2, "foo-1.txt", "foo-2.txt");
+ assertNoMatch(pathExpression2, "foo-5.txt", "foo-11.txt", "sub/foo-3.txt", "sub/sub/foo-4.txt");
}
}
diff --git a/resources/Documentation/path-expressions.md b/resources/Documentation/path-expressions.md
index 322d757..e5c2dd8 100644
--- a/resources/Documentation/path-expressions.md
+++ b/resources/Documentation/path-expressions.md
@@ -7,15 +7,18 @@
Which syntax is used depends on the used code owner backend:
-* [find-owners](backend-find-owners.html) backend: uses [globs](#globs)
-* [proto](backend-proto.html) backend: uses
- [simple path expressions](#simplePathExpressions)
+* [find-owners](backend-find-owners.html) backend:
+ uses [globs](#globs), but each glob is automatically prefixed with `{**/,}`
+ so that subfolders are always matched (e.g. `*.md` matches all md files in all
+ subfolders, rather then only md files in the current folder)
+* [proto](backend-proto.html) backend:
+ uses [simple path expressions](#simplePathExpressions)
## <a id="globs">Globs
Globs support the following wildcards:
-* `*`/`**`: matches any string, including slashes
+* `*`: matches any string, including slashes
* `?`: matches any character
* `[abc]`: matches one character given in the bracket
* `[a-c]`: matches one character from the range given in the bracket
@@ -34,15 +37,15 @@
## <a id="examples">Examples
-| To Match | Glob | Simple Path Expression |
-| -------- | ---- | ---------------------- |
-| Concrete file in current folder | `BUILD` | `BUILD` |
-| File type in current folder | not possible | `*.md` |
-| Concrete file in the current folder and in all subfolders | `{**/,}BUILD` | needs 2 expressions: `BUILD` + `.../BUILD` |
-| File type in the current folder and in all subfolders | `**.md` or `*.md` | `....md` |
-| All files in a subfolder | `my-folder/**` | `my-folder/...` |
-| All “foo-<1-digit-number>.txt” files in all subfolders | `{**/,}foo-[0-9].txt` | not possible |
-| All “foo-<n-digit-number>.txt” files in all subfolders | not possible | not possible |
+| To Match | Glob | find-owners | Simple Path Expression |
+| -------- | ---- | ----------- | ---------------------- |
+| Concrete file in current folder | `BUILD` | not possible | `BUILD` |
+| File type in current folder | `*.md` | not possible | `*.md` |
+| Concrete file in the current folder and in all subfolders | `{**/,}BUILD` | `BUILD` | needs 2 expressions: `BUILD` + `.../BUILD` |
+| File type in the current folder and in all subfolders | `**.md` | `*.md` or `**.md` | `....md` |
+| All files in a subfolder | `my-folder/**` | not possible, but you can add a `my-folder/OWNERS` file instead of using a glob | `my-folder/...` |
+| All “foo-<1-digit-number>.txt” files in all subfolders | `{**/,}foo-[0-9].txt` | `foo-[0-9].txt` |not possible |
+| All “foo-<n-digit-number>.txt” files in all subfolders | not possible | not possible | not possible
---