Accept per-file with multiple globs and emails.
* Enhance Parser regular expressions, and reuse them in OwnersValidator.
* Add more unit test cases with multiple globs and emails.
* Reject not yet implemented "per-file glob = set noparent" syntax.
Change-Id: I289926b2516196e84651a83f7cfa2f6f461ca9e4
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
index 866a2e7..8f2b262 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
@@ -51,8 +51,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
@@ -230,17 +228,6 @@
}
}
- // Line patterns accepted by Parser.java in the find-owners plugin.
- static final Pattern patComment = Pattern.compile("^ *(#.*)?$");
- static final Pattern patEmail = // email address or a "*"
- Pattern.compile("^ *([^ <>@]+@[^ <>@#]+|\\*) *(#.*)?$");
- static final Pattern patFile = Pattern.compile("^ *file:.*$");
- static final Pattern patNoParent = Pattern.compile("^ *set +noparent *(#.*)?$");
- static final Pattern patPerFileNoParent =
- Pattern.compile("^ *per-file +([^= ]+) *= *set +noparent *(#.*)?$");
- static final Pattern patPerFileEmail =
- Pattern.compile("^ *per-file +([^= ]+) *= *([^ <>@]+@[^ <>@#]+|\\*) *(#.*)?$");
-
private static void collectEmail(
Map<String, Set<String>> map, String email, String file, int lineNumber) {
if (!email.equals("*")) {
@@ -268,17 +255,18 @@
String path,
int lineNumber,
String line) {
- Matcher m;
- if (patComment.matcher(line).find()
- || patNoParent.matcher(line).find()
- || patPerFileNoParent.matcher(line).find()) {
- return;
- } else if ((m = patEmail.matcher(line)).find()) {
- collectEmail(email2lines, m.group(1), path, lineNumber);
- } else if ((m = patPerFileEmail.matcher(line)).find()) {
- collectEmail(email2lines, m.group(2).trim(), path, lineNumber);
+ String email;
+ String[] emails;
+ if (Parser.isComment(line) || Parser.isNoParent(line)) {
+ // no email address to check
+ } else if ((email = Parser.parseEmail(line)) != null) {
+ collectEmail(email2lines, email, path, lineNumber);
+ } else if ((emails = Parser.parsePerFileEmails(line)) != null) {
+ for (String e : emails) {
+ collectEmail(email2lines, e, path, lineNumber);
+ }
} else {
- String prefix = patFile.matcher(line).find() ? "ignored" : "syntax";
+ String prefix = Parser.isFile(line) ? "ignored" : "syntax";
add(messages, prefix + ": " + path + ":" + lineNumber + ": " + line, true);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
index 57f137d..1264e78 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
@@ -28,31 +28,90 @@
* <p>OWNERS file syntax:
*
* <pre>
- * lines := (\s* line? \s* "\n")*
- * line := "set noparent"
- * | "per-file" \s+ glob \s* "=" \s* directive
- * | "file:" glob
- * | comment
- * | directive
- * directive := email_address
- * | "*"
- * glob := [a-zA-Z0-9_-*?]+
- * comment := "#" [^"\n"]*
+ * lines := (\s* line? \s* "\n")*
+ * line := "set" \s+ "noparent"
+ * | "per-file" \s+ globs \s* "=" \s* directives
+ * | "file:" \s* glob
+ * | comment
+ * | directive
+ * directives := directive (comma directive)*
+ * directive := email_address
+ * | "*"
+ * globs := glob (comma glob)*
+ * glob := [a-zA-Z0-9_-*?.]+
+ * comma := \s* "," \s*
+ * comment := "#" [^"\n"]*
* </pre>
*
* <p>The "file:" directive is not implemented yet.
*
- * <p>"per-file glob = directive" applies directive only to files matching glob. glob does not
- * contain directory path.
+ * <p>"per-file globs = directives" applies each directive to files matching any of the globs.
+ * A glob does not contain directory path.
*/
class Parser {
- static final Pattern patComment = Pattern.compile("^ *(#.*)?$");
+ // Globs and emails are separated by commas with optional spaces around a comma.
+ protected static final String COMMA = "[\\s]*,[\\s]*"; // used in unit tests
+
+ private static final String BOL = "^[\\s]*"; // begin-of-line
+ private static final String EOL = "[\\s]*(#.*)?$"; // end-of-line
+ private static final String GLOB = "[^\\s,=]+"; // a file glob
+
// TODO: have a more precise email address pattern.
- static final Pattern patEmail = // email address or a "*"
- Pattern.compile("^ *([^ <>@]+@[^ <>@#]+|\\*) *(#.*)?$");
- static final Pattern patFile = Pattern.compile("^ *file:.*$");
- static final Pattern patNoParent = Pattern.compile("^ *set +noparent(( |#).*)?$");
- static final Pattern patPerFile = Pattern.compile("^ *per-file +([^= ]+) *= *([^#]+).*$");
+ private static final String EMAIL_OR_STAR = "([^\\s<>@,]+@[^\\s<>@#,]+|\\*)";
+
+ // Simple input lines with 0 or 1 sub-pattern.
+ private static final Pattern patComment = Pattern.compile(BOL + EOL);
+ private static final Pattern patEmail = Pattern.compile(BOL + EMAIL_OR_STAR + EOL);
+ private static final Pattern patFile = Pattern.compile(BOL + "file:.*" + EOL);
+ private static final Pattern patNoParent = Pattern.compile(BOL + "set[\\s]+noparent" + EOL);
+
+ // Patterns to match trimmed globs and emails in per-file lines.
+ private static final Pattern patEmailList =
+ Pattern.compile("^(" + EMAIL_OR_STAR + "(" + COMMA + EMAIL_OR_STAR + ")*)$");
+ private static final Pattern patGlobs =
+ Pattern.compile("^(" + GLOB + "(" + COMMA + GLOB + ")*)$");
+ // patPerFile matches a line to two groups: (1) globs, (2) emails
+ // Trimmed 1st group should match patGlobs; trimmed 2nd group should match patEmailList.
+ private static final Pattern patPerFile =
+ Pattern.compile(BOL + "per-file[\\s]+([^=#]+)=[\\s]*([^#]+)" + EOL);
+
+ static boolean isComment(String line) {
+ return patComment.matcher(line).matches();
+ }
+
+ static boolean isFile(String line) {
+ return patFile.matcher(line).matches();
+ }
+
+ static boolean isGlobs(String line) {
+ return patGlobs.matcher(line).matches();
+ }
+
+ static boolean isNoParent(String line) {
+ return patNoParent.matcher(line).matches();
+ }
+
+ static String parseEmail(String line) {
+ Matcher m = Parser.patEmail.matcher(line);
+ return m.matches() ? m.group(1).trim() : null;
+ }
+
+ static String[] parsePerFile(String line) {
+ Matcher m = patPerFile.matcher(line);
+ if (!m.matches() || !isGlobs(m.group(1).trim())
+ || !patEmailList.matcher(m.group(2).trim()).matches()) {
+ return null;
+ }
+ String[] parts = new String[2];
+ parts[0] = m.group(1).trim();
+ parts[1] = m.group(2).trim();
+ return parts;
+ }
+
+ static String[] parsePerFileEmails(String line) {
+ String[] globsAndEmails = parsePerFile(line);
+ return (globsAndEmails != null) ? globsAndEmails[1].split(COMMA) : null;
+ }
static class Result {
boolean stopLooking; // if this file contains set noparent
@@ -88,28 +147,23 @@
*/
static void parseLine(Result result, String dir, String file, String line, int num) {
// comment and file: directive are parsed but ignored.
- if (patNoParent.matcher(line).find()) {
+ String email;
+ String[] globsAndEmails;
+ if (isNoParent(line)) {
result.stopLooking = true;
- } else if (patPerFile.matcher(line).find()) {
- Matcher m = patPerFile.matcher(line);
- m.find();
- parseDirective(result, dir + m.group(1), file, m.group(2).trim(), num);
- } else if (patFile.matcher(line).find()) {
- result.warnings.add(warningMsg(file, num, "ignored", line));
- } else if (patComment.matcher(line).find()) {
+ } else if (isComment(line)) {
// ignore comment and empty lines.
- } else {
- parseDirective(result, dir, file, line, num);
- }
- }
-
- private static void parseDirective(
- Result result, String pathGlob, String file, String line, int num) {
- // A directive is an email address or "*".
- if (patEmail.matcher(line).find()) {
- Matcher m = patEmail.matcher(line);
- m.find();
- Util.addToMap(result.owner2paths, m.group(1), pathGlob);
+ } else if ((email = parseEmail(line)) != null) {
+ Util.addToMap(result.owner2paths, email, dir);
+ } else if ((globsAndEmails = parsePerFile(line)) != null) {
+ String[] emails = globsAndEmails[1].split(COMMA);
+ for (String glob : globsAndEmails[0].split(COMMA)) {
+ for (String e : emails) {
+ Util.addToMap(result.owner2paths, e, dir + glob);
+ }
+ }
+ } else if (isFile(line)) {
+ result.warnings.add(warningMsg(file, num, "ignored", line));
} else {
result.errors.add(errorMsg(file, num, "ignored unknown line", line));
}
diff --git a/src/main/resources/Documentation/syntax.md b/src/main/resources/Documentation/syntax.md
index 54db99f..0cf3ae7 100644
--- a/src/main/resources/Documentation/syntax.md
+++ b/src/main/resources/Documentation/syntax.md
@@ -7,23 +7,27 @@
The syntax is:
```java
-lines := (SPACE* line? SPACE* EOL)*
-line := "set noparent"
- | "per-file" SPACE+ glob SPACE* "=" SPACE* directive
- | comment
- | directive
-directive := email
- | "*"
-glob := [a-zA-Z0-9_-*?]+
-comment := "#" ANYCHAR*
-email := [^ @]+@[^ #]+
-ANYCHAR := any character but EOL
-EOL := end of line characters
-SPACE := any white space character
+lines := (SPACE* line? SPACE* EOL)*
+line := "set" SPACE+ "noparent"
+ | "per-file" SPACE+ globs SPACE* "=" SPACE* directives
+ | comment
+ | directive
+directives := directive (SPACE* "," SPACE* directive)*
+directive := email
+ | "*"
+globs := glob (SPACE* "," SPACE* glob)*
+glob := [a-zA-Z0-9_-*?.]+
+comment := "#" ANYCHAR*
+email := [^ @]+@[^ #]+
+ANYCHAR := any character but EOL
+EOL := end of line characters
+SPACE := any white space character
```
-* `per-file glob = directive` applies `directive` only to files
- matching `glob`, which does not contain directory path.
+* `per-file globs = directives` applies each `directive` only to files
+ matching any of the `glob`.
+
+* A 'glob' does not contain directory path.
* The email addresses should belong to registered Gerrit users.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java
index b8d445a..3403bcc 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java
@@ -135,17 +135,18 @@
+ " user1@google.com # comment\n"
+ "u1+review@g.com###\n"
+ " * # everyone can approve\n"
- + "per-file *.py = set noparent###\n"
- + "per-file *.py=u2.m@g.com\n"
- + "per-file *.txt = * # everyone can approve\n"
- + "set noparent # comment\n");
+ + "per-file *.py=u2.m@g.com \n"
+ + "per-file *.c, *.java ,A*bp = u1@g.com, u1+review@g.com ,u2.m@g.com#comment\n"
+ + " per-file *.txt = * # everyone can approve # \n"
+ + " set noparent # comment#\n");
private static final ImmutableSet<String> EXPECTED_VERBOSE_OUTPUT =
ImmutableSet.of(
"MSG: validate: " + OWNERS,
- "MSG: owner: user1@google.com",
"MSG: owner: u1+review@g.com",
- "MSG: owner: u2.m@g.com");
+ "MSG: owner: u1@g.com",
+ "MSG: owner: u2.m@g.com",
+ "MSG: owner: user1@google.com");
@Test
public void testGoodInput() throws Exception {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
index 6f0ca72..bd50f0a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
+import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -119,27 +120,40 @@
@Test
public void perFileGoodDirectiveTest() {
- String[] directives = {"abc@google.com#comment", " *# comment", " xyz@gmail.com # comment"};
- String[] emails = {"abc@google.com", "*", "xyz@gmail.com"};
+ String[] directives = {
+ "abc@google.com#comment", " *# comment", " xyz@gmail.com # comment",
+ "a@g.com , xyz@gmail.com , * # comment", "*,*#comment", " a@b,c@d "
+ };
+ String[] globsList = {"*", "*,*.c", " *test*.java , *.cc, *.cpp ", "*.bp,*.mk ,A* "};
for (int i = 0; i < directives.length; i++) {
- String line = "per-file *test*.java=" + directives[i];
- Parser.Result result = testLine(line);
- String[] paths = result.owner2paths.get(emails[i]).toArray(new String[1]);
- assertThat(paths.length).isEqualTo(1);
- assertThat(paths[0]).isEqualTo(mockedTestDir() + "*test*.java");
+ for (String globs : globsList) {
+ String line = "per-file " + globs + "=" + directives[i];
+ Parser.Result result = testLine(line);
+ String[] emailList = directives[i].replaceAll("#.*$", "").trim().split(Parser.COMMA);
+ String[] globList = globs.trim().split(Parser.COMMA);
+ Arrays.sort(globList);
+ for (String email : emailList) {
+ String[] paths = result.owner2paths.get(email).toArray(new String[1]);
+ assertThat(paths.length).isEqualTo(globList.length);
+ Arrays.sort(paths);
+ for (int g = 0; g < globList.length; g++) {
+ assertThat(paths[g]).isEqualTo(mockedTestDir() + globList[g]);
+ }
+ }
+ }
}
}
@Test
public void perFileBadDirectiveTest() {
String[] directives = {
- "file://OWNERS", " ** ", "a b@c .co", "a@b@c #com", "a.<b>@zc#", " set noparent "
+ "file://OWNERS", " ** ", "a b@c .co", "a@b@c #com", "a.<b>@zc#", " set noparent ",
+ " , a@b ", "a@b, , c@d #"
};
- String[] errors = {"file://OWNERS", "**", "a b@c .co", "a@b@c", "a.<b>@zc", "set noparent"};
for (int i = 0; i < directives.length; i++) {
String line = "per-file *test*.c=" + directives[i];
Parser.Result result = testLine(line);
- String expected = testLineErrorMsg(errors[i]);
+ String expected = testLineErrorMsg(line);
assertThat(result.warnings).isEmpty();
assertThat(result.errors).hasSize(1);
assertThat(result.errors.get(0)).isEqualTo(expected);