Implement "file:..." statement and directive
* See semantics and examples in updated syntax.md.
* Add new feature:
* Accept new file: statement/directive in OwnersValidator.java
but still not checking the content of included files.
* Parser.FILE_DIRECTIVE defined as one pattern used in per-file.
* Add Parser.IncludeStack to keep track of included project:file.
* Factor out Parser.includeFile to handle all "include" and "file:"
statements or directives.
* Reduce repeated inclusion and parsing:
* Keep readFiles in OwnersDb to read one repository file only once.
* Add Parser.savedResults to avoid parsing a file multiple times.
* Improve log and error messages:
* Shorten log message: () for empty content, (...) for non-empty.
* Keep Parser warnings and errors as sets to avoid duplication.
Sort all unique warnings and errors before dump to logger.
* Rename parseLine:skip messages to
parseLine:errorRecursion and parseLine:useSaved
* Add documents and tests:
* Add more JavaDoc comments to Parser.java.
* Update syntax.md to include "file:" statement and directive.
* Add test cases in FindOwnersIT, ParserTest, OwnersValidatorTest.
* Reduce dependency on myProjectName
Change-Id: I235bbdd70ecaded4b20767eac0abbe27e6061b01
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
index bf5a2c7..2ae18de 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersDb.java
@@ -16,6 +16,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.collect.Ordering;
import com.google.common.collect.Multimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.reviewdb.client.Account;
@@ -100,13 +101,17 @@
// Some hacked CL could have a target branch that is not created yet.
ObjectId id = getBranchId(repo, branch, changeData, logs);
revision = "";
+ // For the same repo and branch id, keep content of all read files to avoid
+ // repeated read. This cache of files should be passed down to the Parser to
+ // avoid reading the same file through "include" or "file:" statements.
+ Map<String, String> readFiles = new HashMap<>();
if (id != null) {
if (!ownersFileName.equals(Config.OWNERS) && branch.equals("refs/heads/master")) {
// If ownersFileName is not the default "OWNERS", and current branch is master,
// this project should have a non-empty root file of that name.
// We added this requirement to detect errors in project config files
// and Gerrit server bugs that return wrong value of "ownersFileName".
- String content = getFile(repo, id, "/" + ownersFileName, logs);
+ String content = getFile(readFiles, repo, projectName, id, "/" + ownersFileName, logs);
String found = "Found";
if (content.isEmpty()) {
String changeId = Config.getChangeId(changeData);
@@ -122,13 +127,20 @@
fileName = Util.addDotPrefix(fileName); // e.g. "./" "./d1/f1" "./d2/d3/"
String dir = Util.getParentDir(fileName); // e.g. "." "./d1" "./d2"
logs.add("findOwnersFileFor:" + fileName);
+ // Multiple changed files can be in one directory, but each directory
+ // is only searched once for an OWNERS file.
+ // However any file (including another OWNERS file) can be included
+ // by OWNERS files in different directories. In that case, the included
+ // file could be parsed multiple times for different "dir".
+ // Since open/read a Gerrit repository file could be slow, getFile should keep
+ // a copy of all read files to avoid repeated accesses of the same file.
while (!readDirs.contains(dir)) {
readDirs.add(dir);
logs.add("findOwnersFileIn:" + dir);
String filePath = dir + "/" + ownersFileName;
- String content = getFile(repo, id, filePath, logs);
+ String content = getFile(readFiles, repo, projectName, id, filePath, logs);
if (content != null && !content.isEmpty()) {
- addFile(projectName, branch, dir + "/", dir + "/" + ownersFileName,
+ addFile(readFiles, projectName, branch, dir + "/", dir + "/" + ownersFileName,
content.split("\\R"));
}
if (stopLooking.contains(dir + "/") || !dir.contains("/")) {
@@ -220,8 +232,9 @@
}
}
- void addFile(String project, String branch, String dirPath, String filePath, String[] lines) {
- Parser parser = new Parser(repoManager, project, branch, filePath, logs);
+ void addFile(Map<String, String> readFiles, String project, String branch,
+ String dirPath, String filePath, String[] lines) {
+ Parser parser = new Parser(readFiles, repoManager, project, branch, filePath, logs);
Parser.Result result = parser.parseFile(dirPath, lines);
if (result.stopLooking) {
stopLooking.add(dirPath);
@@ -238,8 +251,8 @@
add2dir2Globs(Util.getDirName(glob) + "/", glob);
}
if (config.getReportSyntaxError()) {
- result.warnings.forEach(w -> logger.atWarning().log(w));
- result.errors.forEach(w -> logger.atSevere().log(w));
+ Ordering.natural().sortedCopy(result.errors).forEach(e -> logger.atSevere().log(e));
+ Ordering.natural().sortedCopy(result.warnings).forEach(w -> logger.atWarning().log(w));
}
}
@@ -378,46 +391,71 @@
return null;
}
+ private static String findReadFile(Map<String, String> readFiles, String project, String file) {
+ String key = Parser.getFileKey(project, file);
+ if (readFiles != null && readFiles.get(key) != null) {
+ return readFiles.get(key);
+ }
+ return null;
+ }
+
+ private static void saveReadFile(
+ Map<String, String> readFiles, String project, String file, String content) {
+ if (readFiles != null) {
+ readFiles.put(Parser.getFileKey(project, file), content);
+ }
+ }
+
/** Returns file content or empty string; uses project+branch+file names. */
- public static String getRepoFile(GitRepositoryManager repoManager,
+ public static String getRepoFile(Map<String, String> readFiles, GitRepositoryManager repoManager,
String project, String branch, String file, List<String> logs) {
// 'file' must be an absolute path from the root of 'project'.
- logs.add("getRepoFile:" + project + ":" + branch + ":" + file);
- if (repoManager != null) { // ParserTest can call with null repoManager
- try (Repository repo = repoManager.openRepository(new Project.NameKey(project))) {
- ObjectId id = repo.resolve(branch);
- if (id != null) {
- return getFile(repo, id, file, logs);
+ logs.add("getRepoFile:" + Parser.getFileKey(project, branch, file));
+ file = Util.gitRepoFilePath(file);
+ String content = findReadFile(readFiles, project, file);
+ if (content == null) {
+ content = "";
+ if (repoManager != null) { // ParserTest can call with null repoManager
+ try (Repository repo = repoManager.openRepository(new Project.NameKey(project))) {
+ ObjectId id = repo.resolve(branch);
+ if (id != null) {
+ return getFile(readFiles, repo, project, id, file, logs);
+ }
+ logs.add("getRepoFile not found branch " + branch);
+ } catch (Exception e) {
+ logger.atSevere().log("getRepoFile failed to find repository of project %s", project);
+ logException(logs, "getRepoFile", e);
}
- logs.add("getRepoFile not found branch " + branch);
- } catch (Exception e) {
- logger.atSevere().log("getRepoFile failed to find repository of project %s", project);
- logException(logs, "getRepoFile", e);
}
}
- return "";
+ return content;
}
/** Returns file content or empty string; uses Repository. */
- private static String getFile(
- Repository repo, ObjectId id, String file, List<String> logs) {
+ private static String getFile(Map<String, String> readFiles,
+ Repository repo, String project, ObjectId id, String file, List<String> logs) {
file = Util.gitRepoFilePath(file);
- String header = "getFile:" + file;
- try (RevWalk revWalk = new RevWalk(repo)) {
- RevTree tree = revWalk.parseCommit(id).getTree();
- ObjectReader reader = revWalk.getObjectReader();
- TreeWalk treeWalk = TreeWalk.forPath(reader, file, tree);
- if (treeWalk != null) {
- String content = new String(reader.open(treeWalk.getObjectId(0)).getBytes(), UTF_8);
- logs.add(header + ":" + content);
- return content;
+ String content = findReadFile(readFiles, project, file);
+ if (content == null) {
+ content = "";
+ try (RevWalk revWalk = new RevWalk(repo)) {
+ String header = "getFile:" + file;
+ RevTree tree = revWalk.parseCommit(id).getTree();
+ ObjectReader reader = revWalk.getObjectReader();
+ TreeWalk treeWalk = TreeWalk.forPath(reader, file, tree);
+ if (treeWalk != null) {
+ content = new String(reader.open(treeWalk.getObjectId(0)).getBytes(), UTF_8);
+ logs.add(header + ":(...)");
+ } else {
+ logs.add(header + " (NOT FOUND)");
+ }
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log("get file %s", file);
+ logException(logs, "getFile", e);
}
- logs.add(header + " (NOT FOUND)");
- } catch (Exception e) {
- logger.atSevere().withCause(e).log("get file %s", file);
- logException(logs, "getFile", e);
+ saveReadFile(readFiles, project, file, content);
}
- return "";
+ return content;
}
/** Adds a header + exception message to the logs. */
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 c58a9ef..c36afcb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
@@ -244,6 +244,13 @@
messages.add(new CommitValidationMessage(msg, error));
}
+ private static void checkIncludeOrFile(
+ List<CommitValidationMessage> messages, String path, int num, String line) {
+ // TODO: Check if an included file exists and that it has valid syntax.
+ // An included file could be a new file added by a CL and not in the repository yet
+ add(messages, "unchecked: " + path + ":" + num + ": " + Parser.getIncludeOrFile(line), false);
+ }
+
private static void checkLine(
List<CommitValidationMessage> messages,
Map<String, Set<String>> email2lines,
@@ -258,17 +265,14 @@
collectEmail(email2lines, email, path, lineNumber);
} else if ((emails = Parser.parsePerFileOwners(line)) != null) {
for (String e : emails) {
- if (!e.equals(Parser.TOK_SET_NOPARENT)) {
+ if (e.startsWith("file:")) {
+ checkIncludeOrFile(messages, path, lineNumber, line);
+ } else if (!e.equals(Parser.TOK_SET_NOPARENT)) {
collectEmail(email2lines, e, path, lineNumber);
}
}
} else if (Parser.isInclude(line)) {
- // Included "OWNERS" files will be checked by themselves.
- // TODO: Check if the include file path is valid and existence of the included file.
- // TODO: Check an included file syntax if it is not named as the project ownersFileName.
- add(messages, "unchecked: " + path + ":" + lineNumber + ": " + line, false);
- } else if (Parser.isFile(line)) {
- add(messages, "ignored: " + path + ":" + lineNumber + ": " + line, true);
+ checkIncludeOrFile(messages, path, lineNumber, line);
} else {
add(messages, "syntax: " + 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 0bf1cca..b26d1a9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Parser.java
@@ -19,6 +19,7 @@
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
@@ -30,8 +31,17 @@
/**
* Parse lines in an OWNERS file and put them into an OwnersDb.
+ * One Parser object should be created to parse only one OWNERS file.
+ * It keeps repoManager, project, branch, and filePath of the OWNERS
+ * file so it can find files that are included by OWNERS.
*
- * The syntax, semantics, and some examples are included in syntax.md.
+ * The usage pattern is:
+ * Parser parser = new Parser(repoManager, project, branch, repoFilePath);
+ * String content = OwnersDb.getRepoFile(readFiles, repoManager, project,
+ * branch, repoFilePath, logs);
+ * Parser.Result result = parser.parseFile(dirPath, content);
+ *
+ * OWNERS file syntax, semantics, and examples are included in syntax.md.
*/
class Parser {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -58,23 +68,25 @@
private static final String PROJECT_NAME = "([^\\s:]+" + COLUMN + ")?";
// A relative or absolute file path name without any column or space character.
- private static final String FILE_PATH = "([^\\s:]+)";
+ private static final String FILE_PATH = "([^\\s:#]+)";
private static final String PROJECT_AND_FILE = PROJECT_NAME + FILE_PATH;
private static final String SET_NOPARENT = "set[\\s]+noparent";
+ private static final String FILE_DIRECTIVE = "file:[\\s]*" + PROJECT_AND_FILE;
+ private static final String INCLUDE_OR_FILE = "(file:[\\s]*|include[\\s]+)";
+
// Simple input lines with 0 or 1 sub-pattern.
private static final Pattern PAT_COMMENT = Pattern.compile(BOL + EOL);
private static final Pattern PAT_EMAIL = Pattern.compile(BOL + EMAIL_OR_STAR + EOL);
- private static final Pattern PAT_FILE = Pattern.compile(BOL + "file:.*" + EOL);
private static final Pattern PAT_INCLUDE =
- Pattern.compile(BOL + "include[\\s]+" + PROJECT_AND_FILE + EOL);
+ Pattern.compile(BOL + INCLUDE_OR_FILE + PROJECT_AND_FILE + EOL);
private static final Pattern PAT_NO_PARENT = Pattern.compile(BOL + SET_NOPARENT + EOL);
// Patterns to match trimmed globs, emails, and set noparent in per-file lines.
private static final Pattern PAT_PER_FILE_OWNERS =
- Pattern.compile("^(" + EMAIL_LIST + "|(" + SET_NOPARENT + "))$");
+ Pattern.compile("^(" + EMAIL_LIST + "|" + SET_NOPARENT + "|" + FILE_DIRECTIVE + ")$");
private static final Pattern PAT_GLOBS =
Pattern.compile("^(" + GLOB + "(" + COMMA + GLOB + ")*)$");
// PAT_PER_FILE matches a line to two groups: (1) globs, (2) emails
@@ -82,41 +94,80 @@
// trimmed 2nd group should match PAT_PER_FILE_OWNERS.
private static final Pattern PAT_PER_FILE =
Pattern.compile(BOL + "per-file[\\s]+([^=#]+)=[\\s]*([^#]+)" + EOL);
+ // Fetch the include/file part of a line with correct syntax.
+ private static final Pattern PAT_INCLUDE_OR_FILE =
+ Pattern.compile("^.*(" + INCLUDE_OR_FILE + PROJECT_AND_FILE + ")" + EOL);
- // A parser keeps current repoManager, project, branch, included file path, and debug/trace logs.
+ // A parser keeps current readFiles, repoManager, project, branch,
+ // included file path, and debug/trace logs.
+ private Map<String, String> readFiles;
private GitRepositoryManager repoManager;
private String branch; // All owners files are read from the same branch.
- private Deque<String[]> includeStack; // Keeps include stack of [projectName,filePath].
- private Set<String> included; // Keeps included files of projectName:filePath
+ private IncludeStack stack; // a stack of including files.
private List<String> logs; // Keeps debug/trace messages.
+ private Map<String, Result> savedResults; // projectName:filePath => Parser.Result
- Parser(GitRepositoryManager repoManager, String project, String branch, String file) {
- init(repoManager, project, branch, file, new ArrayList<>());
+ static class IncludeStack {
+ Deque<String> projectName; // project/repository name of included file
+ Deque<String> filePath; // absolute or relative path of included file
+ Set<String> allFiles; // to detect recursive inclusion quickly
+
+ IncludeStack(String project, String file) {
+ projectName = new ArrayDeque<>();
+ filePath = new ArrayDeque<>();
+ allFiles = new HashSet<>();
+ push(project, file);
+ }
+
+ void push(String project, String file) {
+ projectName.push(project);
+ filePath.push(file);
+ allFiles.add(getFileKey(project, file));
+ }
+
+ void pop() {
+ allFiles.remove(getFileKey(currentProject(), currentFile()));
+ projectName.pop();
+ filePath.pop();
+ }
+
+ String currentProject() {
+ return projectName.peek();
+ }
+
+ String currentFile() {
+ return filePath.peek();
+ }
+
+ boolean contains(String project, String file) {
+ return allFiles.contains(getFileKey(project, file));
+ }
}
- Parser(GitRepositoryManager repoManager,
- String project, String branch, String file, List<String> logs) {
- init(repoManager, project, branch, file, logs);
+ Parser(Map<String, String> readFiles, GitRepositoryManager repoManager,
+ String project, String branch, String file) {
+ init(readFiles, repoManager, project, branch, file, new ArrayList<>());
}
- private void init(GitRepositoryManager repoManager,
+ Parser(Map<String, String> readFiles, GitRepositoryManager repoManager,
String project, String branch, String file, List<String> logs) {
+ init(readFiles, repoManager, project, branch, file, logs);
+ }
+
+ private void init(Map<String, String> readFiles, GitRepositoryManager repoManager,
+ String project, String branch, String file, List<String> logs) {
+ this.readFiles = readFiles;
this.repoManager = repoManager;
this.branch = branch;
this.logs = logs;
- includeStack = new ArrayDeque<>();
- included = new HashSet<>();
- pushProjectFilePaths(project, normalizedRepoDirFilePath(".", file));
+ stack = new IncludeStack(project, normalizedRepoDirFilePath(".", file));
+ savedResults = new HashMap<>();
}
static boolean isComment(String line) {
return PAT_COMMENT.matcher(line).matches();
}
- static boolean isFile(String line) {
- return PAT_FILE.matcher(line).matches();
- }
-
static boolean isInclude(String line) {
return PAT_INCLUDE.matcher(line).matches();
}
@@ -139,18 +190,22 @@
if (!m.matches()) {
return null;
}
- String[] parts = new String[]{m.group(1), m.group(2).trim()};
- if (parts[0] != null && parts[0].length() > 0) {
- // parts[0] has project name followed by ':'
- parts[0] = parts[0].split(COLUMN, -1)[0].trim();
- } else {
- parts[0] = project; // default project name
+ String keyword = m.group(1).trim();
+ if (keyword.equals("file:")) {
+ keyword = "file";
}
- return parts;
+ String projectName = m.group(2);
+ if (projectName != null && projectName.length() > 1) {
+ // PROJECT_NAME ends with ':'
+ projectName = projectName.split(COLUMN, -1)[0].trim();
+ } else {
+ projectName = project; // default project name
+ }
+ return new String[]{keyword, projectName, m.group(3).trim()};
}
static String removeExtraSpaces(String s) {
- return s.trim().replaceAll("[\\s]+", " ");
+ return s.trim().replaceAll("[\\s]+", " ").replaceAll("[\\s]*:[\\s]*", ":");
}
static String[] parsePerFile(String line) {
@@ -167,37 +222,60 @@
return (globsAndOwners != null) ? globsAndOwners[1].split(COMMA, -1) : null;
}
+ static String getIncludeOrFile(String line) {
+ Matcher m = PAT_INCLUDE_OR_FILE.matcher(line);
+ return m.matches() ? removeExtraSpaces(m.group(1)) : "";
+ }
+
static class Result {
boolean stopLooking; // if this file contains set noparent
- List<String> warnings; // warning messages
- List<String> errors; // error messages
+ Set<String> warnings; // unique warning messages
+ Set<String> errors; // unique error messages
Map<String, Set<String>> owner2paths; // maps from owner email to pathGlobs
Set<String> noParentGlobs; // per-file dirpath+glob with "set noparent"
Result() {
stopLooking = false;
- warnings = new ArrayList<>();
- errors = new ArrayList<>();
+ warnings = new HashSet<>();
+ errors = new HashSet<>();
owner2paths = new HashMap<>();
noParentGlobs = new HashSet<>();
}
- void append(Result r) {
+ void append(Result r, String dir, boolean addAll) {
+ // addAll is true when the Result is from an include statements.
+ // It is false for the included result of "file:" directive, which
+ // only collects owner emails, not per-file or set noparent statement.
warnings.addAll(r.warnings);
errors.addAll(r.errors);
- stopLooking |= r.stopLooking; // included file's "set noparent" applies to the including file
- for (String key : r.owner2paths.keySet()) {
- Util.addAllToMap(owner2paths, key, r.owner2paths.get(key));
+ if (addAll) {
+ stopLooking = stopLooking || r.stopLooking;
+ for (String glob : r.noParentGlobs) {
+ noParentGlobs.add(dir + glob);
+ }
}
- noParentGlobs.addAll(r.noParentGlobs);
+ for (String key : r.owner2paths.keySet()) {
+ for (String path : r.owner2paths.get(key)) {
+ // In an included file, top-level owener emails have empty dir path.
+ if (path.isEmpty() || addAll) {
+ Util.addToMap(owner2paths, key, dir + path);
+ }
+ }
+ }
}
}
- // Parse an OWNERS file or included file content in lines.
- // "dir" is the directory that contains "changed files" of a CL,
- // not necessarily the OWNERS or included file directory.
- // "owners" listed in lines control changed files in 'dir' not
- // necessrily the files in the directory containing "lines".
+ /**
+ * Parse given lines of an OWNERS files; return parsed Result.
+ * It can recursively call itself to parse included files.
+ *
+ * @param dir is the directory that contains "changed files" of a CL,
+ * not necessarily the OWNERS or included file directory.
+ * "owners" found in lines control changed files in 'dir'.
+ * 'dir' ends with '/' or is empty when parsing an included file.
+ * @param lines are the source lines of the file to be parsed.
+ * @return the parsed data
+ */
Result parseFile(String dir, String[] lines) {
Result result = new Result();
int n = 0;
@@ -207,25 +285,8 @@
return result;
}
- private String currentProject() {
- return includeStack.peek()[0];
- }
-
- private String currentFilePath() {
- return includeStack.peek()[1];
- }
-
- private String combineProjectAndFile(String project, String file) {
- return project + ":" + file;
- }
-
- private void pushProjectFilePaths(String project, String file) {
- includeStack.push(new String[]{project, file});
- included.add(combineProjectAndFile(project, file));
- }
-
- private void popProjectFilePaths() {
- includeStack.pop();
+ Result parseFile(String dir, String content) {
+ return parseFile(dir, content.split("\\R"));
}
private String normalizedRepoDirFilePath(String dir, String path) {
@@ -240,7 +301,8 @@
}
/**
- * Parse a line in OWNERS file and add info to OwnersDb.
+ * Parse a line in OWNERS file and add parsed info into result.
+ * This function should be called only by parseFile and Parser unit tests.
*
* @param result a Result object to keep parsed info.
* @param dir the path to OWNERS file directory.
@@ -250,56 +312,111 @@
void parseLine(Result result, String dir, String line, int num) {
String email;
String[] globsAndOwners;
- String[] projectAndFile;
+ String[] parsedKPF; // parsed keyword, projectName, filePath
if (isNoParent(line)) {
result.stopLooking = true;
} else if (isComment(line)) {
// ignore comment and empty lines.
} else if ((email = parseEmail(line)) != null) {
- Util.addToMap(result.owner2paths, email, dir);
+ Util.addToMap(result.owner2paths, email, dir); // here dir is not a glob
} else if ((globsAndOwners = parsePerFile(line)) != null) {
- String[] owners = globsAndOwners[1].split(COMMA, -1);
- for (String glob : globsAndOwners[0].split(COMMA, -1)) {
- for (String e : owners) {
- if (e.equals(Parser.TOK_SET_NOPARENT)) {
- result.noParentGlobs.add(dir + glob);
- } else {
- Util.addToMap(result.owner2paths, e, dir + glob);
+ String[] dirGlobs = globsAndOwners[0].split(COMMA, -1);
+ String directive = globsAndOwners[1];
+ if (directive.equals(Parser.TOK_SET_NOPARENT)) {
+ // per-file globs = set noparent
+ for (String glob : dirGlobs) {
+ result.noParentGlobs.add(dir + glob);
+ }
+ } else {
+ List<String> ownerEmails;
+ if ((parsedKPF = parseInclude(stack.currentProject(), directive)) == null) {
+ // per-file globs = ownerEmails
+ ownerEmails = Arrays.asList(directive.split(COMMA, -1));
+ } else {
+ // per-file globs = file: projectFile
+ ownerEmails = new ArrayList<>();
+ Result r = new Result();
+ includeFile(r, "", num, parsedKPF, false);
+ for (String key : r.owner2paths.keySet()) {
+ for (String path : r.owner2paths.get(key)) {
+ if (path.isEmpty()) {
+ ownerEmails.add(key);
+ break;
+ }
+ }
+ }
+ }
+ for (String glob : dirGlobs) {
+ for (String owner: ownerEmails) {
+ Util.addToMap(result.owner2paths, owner, dir + glob);
}
}
}
- } else if (isFile(line)) {
- // file: directive is parsed but ignored.
- result.warnings.add(warningMsg(currentFilePath(), num, "ignored", line));
- logs.add("parseLine:file");
- } else if ((projectAndFile = parseInclude(currentProject(), line)) != null) {
- String project = projectAndFile[0];
- String file = projectAndFile[1];
- String includePath = combineProjectAndFile(project, file);
- // Like C/C++ #include, when f1 includes f2 with a relative path projectAndFile[1],
- // use f1's directory, not 'dir', as the base for relative path.
- // 'dir' is the directory of OWNERS file, which might include f1 indirectly.
- String repoFile = normalizedRepoDirFilePath(Util.getParentDir(currentFilePath()), file);
- String repoProjectFile = combineProjectAndFile(project, repoFile);
- if (included.contains(repoProjectFile)) {
- logs.add("parseLine:skip:include:" + includePath);
- } else {
- pushProjectFilePaths(project, repoFile);
- logs.add("parseLine:include:" + includePath);
- String content =
- OwnersDb.getRepoFile(repoManager, project, branch, repoFile, logs);
- if (content != null && !content.isEmpty()) {
- result.append(parseFile(dir, content.split("\\R")));
- } else {
- logs.add("parseLine:include:(empty)");
- }
- popProjectFilePaths();
- }
+ } else if ((parsedKPF = parseInclude(stack.currentProject(), line)) != null) {
+ includeFile(result, dir, num, parsedKPF, parsedKPF[0].equals("include"));
} else {
- result.errors.add(errorMsg(currentFilePath(), num, "ignored unknown line", line));
+ result.errors.add(errorMsg(stack.currentFile(), num, "ignored unknown line", line));
}
}
+ /**
+ * Find and parse an included file and append data to the 'result'.
+ * For an 'include' statement, parsed data is all appended to the given result parameter.
+ * For a 'file:' statement or directive, only owner emails are appended.
+ * If the project+file name is found in the stored result set, the stored result is reused.
+ * The inclusion is skipped if the to be included file is already on the including file stack.
+ *
+ * @param result to where the included file data should be added.
+ * @param dir the including file's directory or glob.
+ * @param num source code line number
+ * @param parsedKPF the parsed line of include or file directive.
+ * @param addAll to add all parsed data into result or not.
+ */
+ private void includeFile(Result result, String dir, int num, String[] parsedKPF, boolean addAll) {
+ String keyword = parsedKPF[0];
+ String project = parsedKPF[1];
+ String file = parsedKPF[2];
+ String includeKPF = keyword + ":" + getFileKey(project, file);
+ // Like C/C++ #include, when f1 includes f2 with a relative file path,
+ // use f1's directory, not 'dir', as the base for relative path.
+ // 'dir' is the directory of OWNERS file, which might include f1 indirectly.
+ String repoFile = normalizedRepoDirFilePath(Util.getParentDir(stack.currentFile()), file);
+ if (stack.contains(project, repoFile)) {
+ logs.add("parseLine:errorRecursion:" + includeKPF);
+ result.errors.add(errorMsg(stack.currentFile(), num, "recursive include", includeKPF));
+ return;
+ }
+ String savedResultKey = getFileKey(project, repoFile);
+ Result includedFileResult = savedResults.get(savedResultKey);
+ if (null != includedFileResult) {
+ logs.add("parseLine:useSaved:" + includeKPF);
+ } else {
+ stack.push(project, repoFile);
+ logs.add("parseLine:" + includeKPF);
+ String content =
+ OwnersDb.getRepoFile(readFiles, repoManager, project, branch, repoFile, logs);
+ if (content != null && !content.isEmpty()) {
+ includedFileResult = parseFile("", content);
+ } else {
+ logs.add("parseLine:" + keyword + ":()");
+ includedFileResult = new Result();
+ }
+ stack.pop();
+ savedResults.put(savedResultKey, includedFileResult);
+ }
+ result.append(includedFileResult, dir, addAll);
+ }
+
+ // Build a readable key or output string for a (project, file) pair.
+ static String getFileKey(String project, String file) {
+ return project + ":" + file;
+ }
+
+ // Build a readable key or output string for a (project, branch, file) tuple.
+ static String getFileKey(String project, String branch, String file) {
+ return project + ":" + branch + ":" + file;
+ }
+
private static String createMsgLine(String prefix, String file, int n, String msg, String line) {
return prefix + file + ":" + n + ": " + msg + ": [" + line + "]";
}
diff --git a/src/main/resources/Documentation/syntax.md b/src/main/resources/Documentation/syntax.md
index 1470ed2..ac1637e 100644
--- a/src/main/resources/Documentation/syntax.md
+++ b/src/main/resources/Documentation/syntax.md
@@ -7,26 +7,30 @@
### Syntax
```java
-lines := (SPACE* line? SPACE* EOL)*
-line := comment
- | noparent
- | ownerEmail
- | "per-file" SPACE+ globs SPACE* "=" SPACE* owners
- | "include" SPACE+ (project SPACE* ":" SPACE*)? filePath
-comment := "#" ANYCHAR*
-noparent := "set" SPACE+ "noparent"
-ownerEmail := email
- | "*"
-owners := noparent
- | ownerEmail (SPACE* "," SPACE* ownerEmail)*
-globs := glob (SPACE* "," SPACE* glob)*
-glob := [a-zA-Z0-9_-*?.]+
-email := [^ @]+@[^ #]+
-project := a Gerrit project name without space or column character
-filePath := a file path name without space or column character
-ANYCHAR := any character but EOL
-EOL := end of line characters
-SPACE := any white space character
+lines := (SPACE* line? SPACE* EOL)*
+line := comment
+ | noparent
+ | ownerEmail
+ | "per-file" SPACE+ globs SPACE* "=" SPACE* owners
+ | include projectFile
+include := "include" SPACE+
+ | "file:" SPACE*
+projectFile := (project SPACE* ":" SPACE*)? filePath
+comment := "#" ANYCHAR*
+noparent := "set" SPACE+ "noparent"
+ownerEmail := email
+ | "*"
+owners := noparent
+ | "file:" SPACE* projectFile
+ | ownerEmail (SPACE* "," SPACE* ownerEmail)*
+globs := glob (SPACE* "," SPACE* glob)*
+glob := [a-zA-Z0-9_-*?.]+
+email := [^ @]+@[^ #]+
+project := a Gerrit project name without space or column character
+filePath := a file path name without space or column character
+ANYCHAR := any character but EOL
+EOL := end of line characters
+SPACE := any white space character
```
* An OWNERS file can include another file with the `include filePath`
@@ -39,8 +43,31 @@
of the current including file directory and then searched from the
(given) project root directory.
-* An OWNERS file inherit rules in OWNERS files of parent directories
+* The `file: ...` statement is similar to `include ...`
+ * To be compatible to Google's old OWNERS `file:` directive,
+ the `file: ...` statement ignores the `per-file ...` and
+ `set noparent` lines in the directly or indirectly included files.
+ The `include ...` statement takes all statements in the
+ directly included file.
+ * The `include ...` statement is more general and should be
+ used in new OWNERS files.
+ * Google's `file: <file>` directive only includes files
+ in the same repository. The syntax does not accept a
+ "project path" like the `include ...` statement.
+ * Google's `file: <file>` directive requires the include file
+ to have a name with the 'OWNERS' substring,
+ but `include ...` statement can include any file.
+
+* Recursive inclusion (loop) is an error, which is detected
+ and logged as a run-time error. The inclusion is then ignored.
+
+* Repeated inclusion (non-loop) is allowed but redundant.
+ The effect is similar to duplicated statements in an OWNERS file.
+
+* An OWNERS file inherits rules in OWNERS files of parent directories
by default, unless `set noparent` is specified.
+ Note that when a file is included into other files,
+ it does not inherit rules in its parent directory's OWNERS files.
* A "glob" is UNIX globbing pathname without the directory path.
@@ -50,11 +77,15 @@
* `per-file globs = set noparent` is like `set noparent` but applies only to
files matching any of the `globs`. OWNERS files in parent directories
- are not considrered for files matching those globs. Even default ownerEmails
+ are not considered for files matching those globs. Even default ownerEmails
specified in the current OWNERS file are not included.
-* Without the `per-file globs = set noparent`, all global ownerEmails also
- apply to files matching those globs.
+* Without the `per-file globs = set noparent`, all non-per-file
+ ownerEmails also apply to files matching those globs.
+
+* `per-file globs = file: <projectFile>` is equivalent to
+ `per-file globs = emails`, where the emails are collected
+ from top-level emails collected from `file: <projectFile>`.
* The email addresses should belong to registered Gerrit users.
A group mailing address can be used as long as it is associated to
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
index 3233024..e7fef5b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/FindOwnersIT.java
@@ -113,28 +113,26 @@
// Submitted c2 still finds no owners before c1 is submitted.
assertThat(getOwnersResponse(c2)).contains("owners:[], files:[ t.c ]");
PushOneCommit.Result c1 = addFile("2", "OWNERS",
- "x@x\na@a\nfile:f1.txt\ninclude P1/P2 : f1\ninclude ./d1/d2/../../f2\n");
+ "x@x\na@a\ninclude P1/P2 : f1\ninclude ./d1/d2/../../f2\n");
// Now c2 should find owners, but include directives find no repository or file.
String ownersAX = "owners:[ " + ownerJson("a@a") + ", " + ownerJson("x@x") + " ]";
String path2owners = "path2owners:{ ./:[ a@a, x@x ] }";
String owner2paths = "owner2paths:{ a@a:[ ./ ], x@x:[ ./ ] }";
- String projectName = myProjectName("includeNotFoundTest");
+ String projectName = project.get();
String expectedInLog = "project:" + projectName + ", "
+ "ownersFileName:OWNERS, "
+ "getBranchId:refs/heads/master(FOUND), "
+ "findOwnersFileFor:./t.c, "
+ "findOwnersFileIn:., "
- + "getFile:OWNERS:x@x\\na@a\\nfile:f1.txt\\n"
- + "includeP1/P2:f1\\ninclude./d1/d2/../../f2\\n, "
- + "parseLine:file, "
+ + "getFile:OWNERS:(...), "
+ "parseLine:include:P1/P2:f1, "
+ "getRepoFile:P1/P2:refs/heads/master:f1, "
+ "getRepoFileException:repositorynotfound:P1/P2, " // repository not found
- + "parseLine:include:(empty), " // missing file is treated as empty
+ + "parseLine:include:(), " // missing file is treated as empty
+ "parseLine:include:" + projectName + ":./d1/d2/../../f2, "
+ "getRepoFile:" + projectName + ":refs/heads/master:f2, "
+ "getFile:f2(NOTFOUND), " // same repository but f2 is missing
- + "parseLine:include:(empty), " // missing file is treated as empty
+ + "parseLine:include:(), " // missing file is treated as empty
+ "countNumOwners, "
+ "findOwners, "
+ "checkFile:./t.c, "
@@ -162,7 +160,7 @@
// c2 and c1 are both submitted before existence of OWNERS.
PushOneCommit.Result c2 = addFile("c2", "t.c", "##");
PushOneCommit.Result c1 = addFile("c1", "OWNERS",
- "x@x\na@a\nfile:f1.txt\ninclude P1/P2 : f1\ninclude ./d1/d2/../../f2\n");
+ "x@x\na@a\ninclude P1/P2 : f1\ninclude ./d1/d2/../../f2\n");
String ownerA = ownerJson("a@a");
String ownerX = ownerJson("x@x");
String ownerG1 = ownerJson("g1@g");
@@ -171,22 +169,20 @@
"owners:[ " + ownerA + ", " + ownerG1 + ", " + ownerG2 + ", " + ownerX + " ]";
String path2owners = "path2owners:{ ./:[ a@a, g1@g, g2@g, x@x ] }";
String owner2paths = "owner2paths:{ a@a:[ ./ ], g1@g:[ ./ ], g2@g:[ ./ ], x@x:[ ./ ] }";
- String projectName = myProjectName("includeFoundTest");
+ String projectName = project.get();
String expectedInLog = "project:" + projectName + ", "
+ "ownersFileName:OWNERS, "
+ "getBranchId:refs/heads/master(FOUND), "
+ "findOwnersFileFor:./t.c, "
+ "findOwnersFileIn:., "
- + "getFile:OWNERS:x@x\\na@a\\nfile:f1.txt\\n"
- + "includeP1/P2:f1\\ninclude./d1/d2/../../f2\\n, "
- + "parseLine:file, "
+ + "getFile:OWNERS:(...), "
+ "parseLine:include:P1/P2:f1, "
+ "getRepoFile:P1/P2:refs/heads/master:f1, "
+ "getRepoFileException:repositorynotfound:P1/P2, "
- + "parseLine:include:(empty), " // P1/P2 is still not found
+ + "parseLine:include:(), " // P1/P2 is still not found
+ "parseLine:include:" + projectName + ":./d1/d2/../../f2, "
+ "getRepoFile:" + projectName + ":refs/heads/master:f2, "
- + "getFile:f2:g1@g\\ng2@g\\n, " // f2 is included
+ + "getFile:f2:(...), " // f2 is included
+ "countNumOwners, "
+ "findOwners, "
+ "checkFile:./t.c, "
@@ -231,6 +227,78 @@
}
@Test
+ public void includeVsFileTest() throws Exception {
+ // Test difference between include and file statements.
+ // The file statement skips "set noparent" and "per-file" statements.
+ addFile("d1", "d1/OWNERS", "d1@g\n");
+ addFile("d1/d1", "d1/d1/OWNERS",
+ "per-file *.c=d1d1p@g\nd1d1@g\nfile: d1/OWNERS\n");
+ addFile("d1/d1/d1", "d1/d1/d1/OWNERS",
+ "set noparent\nper-file *.c=d1d1d1p@g\nd1d1d1@g\n");
+ addFile("d1/d2", "d1/d2/OWNERS",
+ "per-file *.c=d1d2p@g\nd1d2@g\ninclude d1/OWNERS\n");
+ addFile("d1/d2/d1", "d1/d2/d1/OWNERS",
+ "set noparent\nper-file *.c=d1d2d1p@g\nd1d2d1@g\n");
+
+ addFile("d2", "d2/OWNERS", "d2@g\n");
+ addFile("d2/d1", "d2/d1/OWNERS",
+ "per-file *.c=d2d1p@g\nd2d1@g\nfile: ./d1/OWNERS\n");
+ addFile("d2/d1/d1", "d2/d1/d1/OWNERS",
+ "set noparent\nper-file *.c=d2d1d1p@g\nd2d1d1@g\n");
+ addFile("d2/d2", "d2/d2/OWNERS",
+ "per-file *.c=d2d2p@g\nd2d2@g\ninclude ./d1/OWNERS\n");
+ addFile("d2/d2/d1", "d2/d2/d1/OWNERS",
+ "set noparent\nper-file *.c=d2d2d1p@g\nd2d2d1@g\n");
+
+ addFile("d3", "d3/OWNERS", "d3@g\n");
+ addFile("d3/d1/d1", "d3/d1/d1/OWNERS", "d3d1d1@g\nfile: ../../../d1/d1/OWNERS\n");
+ addFile("d3/d1/d2", "d3/d1/d2/OWNERS", "d3d1d2@g\nfile: //d1/d2/OWNERS\n");
+ addFile("d3/d2/d1", "d3/d2/d1/OWNERS", "d3d2d1@g\ninclude /d2/d1/OWNERS\n");
+ addFile("d3/d2/d2", "d3/d2/d2/OWNERS", "d3d2d2@g\ninclude //d2/d2/OWNERS\n");
+ PushOneCommit.Result c11 = createChange("c11", "d3/d1/d1/t.c", "test");
+ PushOneCommit.Result c12 = createChange("c12", "d3/d1/d2/t.c", "test");
+ PushOneCommit.Result c21 = createChange("c21", "d3/d2/d1/t.c", "test");
+ PushOneCommit.Result c22 = createChange("c22", "d3/d2/d2/t.c", "test");
+
+ // file and file
+ String owners11 = "file2owners:{ ./d3/d1/d1/t.c:"
+ + "[ d1d1@g, d1d1d1@g, d3@g, d3d1d1@g ] }";
+ // file and include
+ String owners12 = "file2owners:{ ./d3/d1/d2/t.c:"
+ + "[ d1d2@g, d1d2d1@g, d3@g, d3d1d2@g ] }";
+ // include and file
+ String owners21 = "file2owners:{ ./d3/d2/d1/t.c:"
+ + "[ d2d1@g, d2d1d1@g, d2d1p@g, d3@g, d3d2d1@g ] }";
+ // include and include
+ String owners22 = "file2owners:{ ./d3/d2/d2/t.c:"
+ + "[ d2d2@g, d2d2d1@g, d2d2d1p@g, d2d2p@g, d3d2d2@g ] }";
+
+ assertThat(getOwnersDebugResponse(c11)).contains(owners11);
+ assertThat(getOwnersDebugResponse(c12)).contains(owners12);
+ assertThat(getOwnersDebugResponse(c21)).contains(owners21);
+ assertThat(getOwnersDebugResponse(c22)).contains(owners22);
+ }
+
+ @Test
+ public void multipleIncludeTest() throws Exception {
+ // Now "include" and "file:" statements can share parsed results.
+ addFile("d1", "d1/OWNERS", "d1@g\n");
+ addFile("d2/d1", "d2/d1/OWNERS", "include /d1/OWNERS\nfile://d1/OWNERS\n");
+ addFile("d2/d2", "d2/d2/OWNERS", "file: //d1/OWNERS\ninclude /d1/OWNERS\n");
+ PushOneCommit.Result c1 = createChange("c1", "d2/d1/t.c", "test");
+ PushOneCommit.Result c2 = createChange("c2", "d2/d2/t.c", "test");
+ String projectName = project.get();
+ String log1 = "parseLine:useSaved:file:" + projectName + "://d1/OWNERS, ";
+ String log2 = "parseLine:useSaved:include:" + projectName + ":/d1/OWNERS, ";
+ String response1 = getOwnersDebugResponse(c1);
+ String response2 = getOwnersDebugResponse(c2);
+ assertThat(response1).contains(log1);
+ assertThat(response1).doesNotContain(log2);
+ assertThat(response2).doesNotContain(log1);
+ assertThat(response2).contains(log2);
+ }
+
+ @Test
public void includeCycleTest() throws Exception {
// f1 includes f2, f2 includes f3, f3 includes f4, f4 includes f2, OWNERS includes f1.
// All files are in the root directory, but could be referred with relative paths.
@@ -241,26 +309,26 @@
addFile("5", "OWNERS", "x@g\ninclude ./d1/../f1\n");
PushOneCommit.Result c = createChange("6", "t.c", "#\n");
String response = getOwnersDebugResponse(c);
- String projectName = myProjectName("includeCycleTest");
+ String projectName = project.get();
String expectedInLog = "project:" + projectName + ", "
+ "ownersFileName:OWNERS, "
+ "getBranchId:refs/heads/master(FOUND), "
+ "findOwnersFileFor:./t.c, "
+ "findOwnersFileIn:., "
- + "getFile:OWNERS:x@g\\ninclude./d1/../f1\\n, "
+ + "getFile:OWNERS:(...), "
+ "parseLine:include:" + projectName + ":./d1/../f1, "
+ "getRepoFile:" + projectName + ":refs/heads/master:f1, "
- + "getFile:f1:f1@g\\ninclude./f2\\n, "
+ + "getFile:f1:(...), "
+ "parseLine:include:" + projectName + ":./f2, "
+ "getRepoFile:" + projectName + ":refs/heads/master:f2, "
- + "getFile:f2:f2@g\\nincluded1/../f3\\n, "
+ + "getFile:f2:(...), "
+ "parseLine:include:" + projectName + ":d1/../f3, "
+ "getRepoFile:" + projectName + ":refs/heads/master:f3, "
- + "getFile:f3:f3@g\\ninclude/f4\\n, "
+ + "getFile:f3:(...), "
+ "parseLine:include:" + projectName + ":/f4, "
+ "getRepoFile:" + projectName + ":refs/heads/master:f4, "
- + "getFile:f4:f4@g\\nincluded2/../f2\\n, "
- + "parseLine:skip:include:" + projectName + ":d2/../f2, "
+ + "getFile:f4:(...), "
+ + "parseLine:errorRecursion:include:" + projectName + ":d2/../f2, "
+ "countNumOwners, "
+ "findOwners, "
+ "checkFile:./t.c, "
@@ -289,42 +357,41 @@
+ "include ../d2/f2\ninclude /d2/d3/f3\ninclude /d2/../d4/d5/f5\ninclude /d4/f4\n");
String result = getOwnersDebugResponse(c);
assertThat(result).contains("{ ./d6/OWNERS:[ f0@g, f1@g, f2@g, f3@g, f4@g, f5@g, f6@g ] }");
- String skipLog = "parseLine:skip:include:" + myProjectName("includeDuplicationTest") + ":";
+ String projectName = project.get();
+ String skipLog = "parseLine:useSaved:include:" + projectName + ":";
for (String path : new String[]{"../../d0/f0", "../d0/f0", "../d2/f2", "/d2/f2", "/d4/f4"}) {
assertThat(result).contains(skipLog + path);
}
- String projectName = myProjectName("includeDuplicationTest");
String expectedInLog = "project:" + projectName + ", "
+ "ownersFileName:OWNERS, "
+ "getBranchId:refs/heads/master(FOUND), "
+ "findOwnersFileFor:./d6/OWNERS, "
+ "findOwnersFileIn:./d6, "
- + "getFile:d6/OWNERS:f6@g\\ninclude/d0/f0\\ninclude../d1/d2/f1\\ninclude../d2/f2\\n"
- + "include/d2/d3/f3\\ninclude/d2/../d4/d5/f5\\ninclude/d4/f4\\n, "
+ + "getFile:d6/OWNERS:(...), "
+ "parseLine:include:" + projectName + ":/d0/f0, "
+ "getRepoFile:" + projectName + ":refs/heads/master:d0/f0, "
- + "getFile:d0/f0:f0@g\\n, "
+ + "getFile:d0/f0:(...), "
+ "parseLine:include:" + projectName + ":../d1/d2/f1, "
+ "getRepoFile:" + projectName + ":refs/heads/master:d1/d2/f1, "
- + "getFile:d1/d2/f1:f1@g\\ninclude../../d0/f0\\n, "
- + "parseLine:skip:include:" + projectName + ":../../d0/f0, "
+ + "getFile:d1/d2/f1:(...), "
+ + "parseLine:useSaved:include:" + projectName + ":../../d0/f0, "
+ "parseLine:include:" + projectName + ":../d2/f2, "
+ "getRepoFile:" + projectName + ":refs/heads/master:d2/f2, "
- + "getFile:d2/f2:f2@g\\ninclude../d0/f0\\n, "
- + "parseLine:skip:include:" + projectName + ":../d0/f0, "
+ + "getFile:d2/f2:(...), "
+ + "parseLine:useSaved:include:" + projectName + ":../d0/f0, "
+ "parseLine:include:" + projectName + ":/d2/d3/f3, "
+ "getRepoFile:" + projectName + ":refs/heads/master:d2/d3/f3, "
- + "getFile:d2/d3/f3:f3@g\\ninclude/d0/f0\\n, "
- + "parseLine:skip:include:" + projectName + ":/d0/f0, "
+ + "getFile:d2/d3/f3:(...), "
+ + "parseLine:useSaved:include:" + projectName + ":/d0/f0, "
+ "parseLine:include:" + projectName + ":/d2/../d4/d5/f5, "
+ "getRepoFile:" + projectName + ":refs/heads/master:d4/d5/f5, "
- + "getFile:d4/d5/f5:f5@g\\ninclude/d2/f2\\ninclude../f4\\n, "
- + "parseLine:skip:include:" + projectName + ":/d2/f2, "
+ + "getFile:d4/d5/f5:(...), "
+ + "parseLine:useSaved:include:" + projectName + ":/d2/f2, "
+ "parseLine:include:" + projectName + ":../f4, "
+ "getRepoFile:" + projectName + ":refs/heads/master:d4/f4, "
- + "getFile:d4/f4:f4@g\\ninclude../d2/f2\\n, "
- + "parseLine:skip:include:" + projectName + ":../d2/f2, "
- + "parseLine:skip:include:" + projectName + ":/d4/f4, "
+ + "getFile:d4/f4:(...), "
+ + "parseLine:useSaved:include:" + projectName + ":../d2/f2, "
+ + "parseLine:useSaved:include:" + projectName + ":/d4/f4, "
+ "findOwnersFileIn:., "
+ "getFile:OWNERS(NOTFOUND), "
+ "countNumOwners, "
@@ -340,7 +407,7 @@
@Test
public void ownersPerFileTest() throws Exception {
addFile("1", "OWNERS", "per-file *.c=x@x\na@a\nc@c\nb@b\n");
- // Add "t.c" file, which has per-file owner x@x, not a@a, b@b, c@c.
+ // Add "t.c" file, which has per-file owner x@x, and a@a, b@b, c@c.
PushOneCommit.Result c2 = createChange("2", "t.c", "Hello!");
String ownerA = ownerJson("a@a");
String ownerB = ownerJson("b@b");
@@ -354,6 +421,20 @@
}
@Test
+ public void perFileIncludeTest() throws Exception {
+ // A per-file with file: directive to include more owners.
+ addFile("1", "OWNERS", "per-file *.c=x@x\na@a\nper-file t.c=file: t_owner\n");
+ addFile("2", "t_owner", "t1@g\n*\nper-file *.c=y@y\ninclude more_owner\n");
+ addFile("3", "more_owner", "m@g\nm2@g\nper-file *.c=z@z\n");
+ PushOneCommit.Result c1 = createChange("c1", "x.c", "test");
+ PushOneCommit.Result c2 = createChange("c2", "t.c", "test");
+ String c1Response = getOwnersDebugResponse(c1);
+ String c2Response = getOwnersDebugResponse(c2);
+ assertThat(c1Response).contains("file2owners:{ ./x.c:[ a@a, x@x ] }");
+ assertThat(c2Response).contains("file2owners:{ ./t.c:[ *, a@a, m2@g, m@g, t1@g, x@x ] }");
+ }
+
+ @Test
public void includePerFileTest() throws Exception {
// Test included file with per-file, which affects the including file.
PushOneCommit.Result c1 = addFile("1", "d1/d1/OWNERS", "d1d1@g\nper-file OWNERS=d1d1o@g\n");
@@ -551,7 +632,7 @@
public void projectTest() throws Exception {
RestResponse response = adminRestSession.get("/projects/?d");
String content = response.getEntityContent();
- // Predefined projects: "All-Projects", "All-Users", myProjectName("projectTest")
+ // Predefined projects: "All-Projects", "All-Users", project
assertThat(content).contains("\"id\": \"All-Projects\",");
assertThat(content).contains("\"id\": \"All-Users\",");
assertThat(content).contains(idProject("projectTest", "project"));
@@ -887,10 +968,6 @@
return filteredJson(response.getEntityContent());
}
- private String myProjectName(String test) {
- return myProjectName(test, "project");
- }
-
private String myProjectName(String test, String project) {
return this.getClass().getName() + "_" + test + "_" + project;
}
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 b0444bc..a9292b5 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java
@@ -34,6 +34,7 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -146,16 +147,40 @@
}
}
- private static final String INCLUDE_STMT1 = " include p1/p2 : /d1/owners";
+ private static final String[] INCLUDE_STMTS = new String[]{
+ " include p1/p2 : /d1/owners",
+ "include p1/p2://d1/owners #comment",
+ "include ../d2/owners",
+ " per-file *.java = file: //d2/OWNERS.java #",
+ "per-file *.cpp,*cc=file:p1/p2:/c++owners",
+ " file: p1/p2 : /d1/owners ",
+ "file: p1/p2://d1/owners #comment",
+ "file: ../d2/owners",
+ "file: //OWNERS",
+ "file:///OWNERS.android"};
- private static final String INCLUDE_STMT2 = "include ../d2/owners ";
+ private static String allIncludeStatements() {
+ String statement = "";
+ for (String s: INCLUDE_STMTS) {
+ statement += s + "\n";
+ }
+ return statement;
+ };
+
+ private static Set<String> allIncludeMsgs() {
+ Set<String> msgs = new HashSet<>();
+ for (int i = 0; i < INCLUDE_STMTS.length; i++) {
+ msgs.add("MSG: unchecked: OWNERS:" + (i + 1)
+ + ": " + Parser.getIncludeOrFile(INCLUDE_STMTS[i]));
+ }
+ return msgs;
+ };
private static final ImmutableMap<String, String> FILES_WITH_NO_ERROR =
ImmutableMap.of(
OWNERS,
- "\n\n#comments ...\n ### more comments\n"
- + INCLUDE_STMT1 + "\n"
- + INCLUDE_STMT2 + "\n"
+ allIncludeStatements()
+ + "\n\n#comments ...\n ### more comments\n"
+ " user1@google.com # comment\n"
+ "u1+review@g.com###\n"
+ " * # everyone can approve\n"
@@ -165,20 +190,20 @@
+ "per-file *.java = set noparent # \n"
+ " set noparent # comment#\n");
- private static final ImmutableSet<String> EXPECTED_VERBOSE_OUTPUT =
- ImmutableSet.of(
- "MSG: validate: " + OWNERS,
- "MSG: unchecked: OWNERS:5: " + INCLUDE_STMT1,
- "MSG: unchecked: OWNERS:6: " + INCLUDE_STMT2,
- "MSG: owner: u1+review@g.com",
- "MSG: owner: u1@g.com",
- "MSG: owner: u2.m@g.com",
- "MSG: owner: user1@google.com");
+ private static Set<String> allVerboseMsgs() {
+ Set<String> msgs = allIncludeMsgs();
+ msgs.add("MSG: validate: " + OWNERS);
+ msgs.add("MSG: owner: u1+review@g.com");
+ msgs.add("MSG: owner: u1@g.com");
+ msgs.add("MSG: owner: u2.m@g.com");
+ msgs.add("MSG: owner: user1@google.com");
+ return msgs;
+ };
+ private static final ImmutableSet<String> EXPECTED_VERBOSE_OUTPUT =
+ ImmutableSet.copyOf(allVerboseMsgs());
private static final ImmutableSet<String> EXPECTED_NON_VERBOSE_OUTPUT =
- ImmutableSet.of(
- "MSG: unchecked: OWNERS:5: " + INCLUDE_STMT1,
- "MSG: unchecked: OWNERS:6: " + INCLUDE_STMT2);
+ ImmutableSet.copyOf(allIncludeMsgs());
@Test
public void testGoodInput() throws Exception {
@@ -198,27 +223,23 @@
OWNERS,
"\n\n\nwrong syntax\n#comment\nuser1@google.com\n",
"d2/" + OWNERS,
- "u1@g.com\nu3@g.com\n*\n",
- "d3/" + OWNERS,
- "\n\nfile: common/Owners\n");
+ "u1@g.com\n\nu3@g.com\n*\n");
+
private static final ImmutableSet<String> EXPECTED_WRONG_SYNTAX =
ImmutableSet.of(
"ERROR: syntax: " + OWNERS + ":4: wrong syntax",
- "ERROR: unknown: u3@g.com at d2/" + OWNERS + ":2",
- "ERROR: ignored: d3/" + OWNERS + ":3: file: common/Owners");
+ "ERROR: unknown: u3@g.com at d2/" + OWNERS + ":3");
private static final ImmutableSet<String> EXPECTED_VERBOSE_WRONG_SYNTAX =
ImmutableSet.of(
- "MSG: validate: d3/" + OWNERS,
"MSG: validate: d2/" + OWNERS,
"MSG: validate: " + OWNERS,
"MSG: owner: user1@google.com",
"MSG: owner: u1@g.com",
"MSG: owner: u3@g.com",
"ERROR: syntax: " + OWNERS + ":4: wrong syntax",
- "ERROR: unknown: u3@g.com at d2/" + OWNERS + ":2",
- "ERROR: ignored: d3/" + OWNERS + ":3: file: common/Owners");
+ "ERROR: unknown: u3@g.com at d2/" + OWNERS + ":3");
@Test
public void testWrongSyntax() 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 15431fe..c2de856 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/ParserTest.java
@@ -54,7 +54,7 @@
private static Parser.Result testLine(String line) {
Parser.Result result = new Parser.Result();
// Single line parser tests do not need repoManager.
- Parser parser = new Parser(null, mockedProject(), "master", "OWNERS");
+ Parser parser = new Parser(null, null, mockedProject(), "master", "OWNERS");
parser.parseLine(result, mockedTestDir(), line, 3);
return result;
}
@@ -111,15 +111,25 @@
r2.warnings.add(w2);
r1.noParentGlobs.add(b1);
r2.noParentGlobs.add(b2);
- r2.append(r1);
+ assertThat(r1.owner2paths).isEmpty();
+ assertThat(r2.owner2paths).isEmpty();
+ r2.append(r1, "", true);
+ assertThat(r1.owner2paths).isEmpty();
+ assertThat(r2.owner2paths).isEmpty();
assertThat(r2.warnings).containsExactly(w2, w1);
assertThat(r2.noParentGlobs).containsExactly(b2, b1);
assertThat(r1.noParentGlobs).containsExactly(b1);
assertThat(r2.errors).containsExactly(e2, e1);
- r1.append(r2);
- assertThat(r1.warnings).containsExactly(w1, w2, w1);
- assertThat(r1.noParentGlobs).containsExactly(b2, b1); // set union
- assertThat(r1.errors).containsExactly(e1, e2, e1);
+ r1.append(r2, "", true);
+ assertThat(r1.owner2paths).isEmpty();
+ assertThat(r2.owner2paths).isEmpty();
+ // warnings, errors, and noParentGlobs are sets of strings.
+ // containsExactly does not check order of elements.
+ assertThat(r1.warnings).containsExactly(w1, w2);
+ assertThat(r1.warnings).containsExactly(w2, w1);
+ assertThat(r1.noParentGlobs).containsExactly(b2, b1);
+ assertThat(r1.errors).containsExactly(e1, e2);
+ assertThat(r1.errors).containsExactly(e2, e1);
}
@Test
@@ -149,13 +159,20 @@
@Test
public void fileLineTest() {
- // file: directive is not implemented yet.
+ // file: statement should work like include.
String[] lines = {"file://owners", " file: //d1/owner", "file:owner #"};
for (String s : lines) {
Parser.Result result = testLine(s);
- String expected = testLineWarningMsg(s);
- assertThat(result.warnings).containsExactly(expected);
+ assertThat(result.warnings).isEmpty();
+ assertThat(result.errors).isEmpty();
}
+ testOneFileLine("P0", " file: //common/OWNERS #comment", "P0", "//common/OWNERS");
+ testOneFileLine("P1", "file: Other : /Group ", "Other", "/Group");
+ testOneFileLine("P2", "file: /Common/Project: OWNER", "/Common/Project", "OWNER");
+ testOneFileLine("P3", " file: \tP2/D2:/D3/F.txt", "P2/D2", "/D3/F.txt");
+ testOneFileLine("P4", " file: \tP2/D2://D3/F.txt", "P2/D2", "//D3/F.txt");
+ testOneFileLine("P5", "\t file: \t P2/D2:\t/D3/F2.txt\n", "P2/D2", "/D3/F2.txt");
+ testOneFileLine("P6", "file: ../d1/d2/F \n", "P6", "../d1/d2/F");
}
@Test
@@ -173,7 +190,8 @@
String[] directives = {
"abc@google.com#comment", " *# comment", " xyz@gmail.com # comment",
"a@g.com , xyz@gmail.com , * # comment", "*,*#comment", " a@b,c@d ",
- " set noparent ", "\tset\t\tnoparent\t"
+ " set noparent ", "\tset\t\tnoparent\t",
+ "file://java.owners", " file: p1/p2 : /OWNERS "
};
String[] globsList = {"*", "*,*.c", " *test*.java , *.cc, *.cpp ", "*.bp,*.mk ,A* "};
for (String directive : directives) {
@@ -194,6 +212,10 @@
for (String glob : globList) {
assertThat(result.noParentGlobs).contains(mockedTestDir() + glob);
}
+ } else if (e.startsWith("file:")) {
+ // If per-file has file: directive, it cannot have any other directive.
+ assertThat(e).isEqualTo(Parser.removeExtraSpaces(directive));
+ assertThat(owners).hasLength(1);
} else {
String[] paths = result.owner2paths.get(e).toArray(new String[1]);
assertThat(paths).hasLength(globList.length); // should not work for "set noparent"
@@ -210,8 +232,9 @@
@Test
public void perFileBadDirectiveTest() {
String[] directives = {
- "file://OWNERS", " ** ", "a b@c .co", "a@b@c #com", "a.<b>@zc#",
- " , a@b ", "a@b, , c@d #", "a@b, set noparent"
+ " ** ", "a b@c .co", "a@b@c #com", "a.<b>@zc#",
+ " , a@b ", "a@b, , c@d #", "a@b, set noparent",
+ "a@b, file://java.owners", "*,file:OWNERS"
};
for (String directive : directives) {
String line = "per-file *test*.c=" + directive;
@@ -222,12 +245,21 @@
}
}
- private void testOneIncludeLine(
- String project, String line, String parsedProject, String parsedFile) {
+ private void testOneIncludeOrFileLine(
+ String project, String line, String keyword, String projectName, String filePath) {
String[] results = Parser.parseInclude(project, line);
- assertThat(results).hasLength(2);
- assertThat(results[0]).isEqualTo(parsedProject);
- assertThat(results[1]).isEqualTo(parsedFile);
+ assertThat(results).hasLength(3);
+ assertThat(results[0]).isEqualTo(keyword);
+ assertThat(results[1]).isEqualTo(projectName);
+ assertThat(results[2]).isEqualTo(filePath);
+ }
+
+ private void testOneFileLine(String project, String line, String projectName, String filePath) {
+ testOneIncludeOrFileLine(project, line, "file", projectName, filePath);
+ }
+
+ private void testOneIncludeLine(String project, String line, String projectName, String filePath) {
+ testOneIncludeOrFileLine(project, line, "include", projectName, filePath);
}
@Test
@@ -241,6 +273,31 @@
}
@Test
+ public void getIncludeOrFileTest() {
+ String[] line = new String[] {
+ "", "wrong input", "INCLUDE X",
+ "include //f2.txt # ",
+ " include P1/P2: ../f1 # ",
+ " file://f3 # ",
+ "file: P1:f3",
+ " per-file *.c,file.c = file: /OWNERS # ",
+ "per-file *=file:P1/P2: /O# ",
+ };
+ String[] expected = new String[] {
+ "", "", "",
+ "include //f2.txt",
+ "include P1/P2:../f1",
+ "file://f3",
+ "file:P1:f3",
+ "file:/OWNERS",
+ "file:P1/P2:/O",
+ };
+ for (int i = 0; i < line.length; i++) {
+ assertThat(Parser.getIncludeOrFile(line[i])).isEqualTo(expected[i]);
+ }
+ }
+
+ @Test
public void errorMsgTest() {
String file = "./OWNERS";
int n = 5;