Add OwnersValidator to check changed OWNERS.
* Checks OWNERS file syntax and email addresses used in OWNERS files.
Emails should belong to accounts found on the Gerrit server.
* This check is disabled by default and enabled in project.config with
"rejectErrorInOwners = true".
Change-Id: I2fe59bd622dd118d4c59bbf6a1fb574b3a58a896
diff --git a/BUILD b/BUILD
index eb304bb..72c2671 100644
--- a/BUILD
+++ b/BUILD
@@ -40,6 +40,7 @@
# resources = glob(['src/test/resources/**/*']),
tags = ['findowners'],
deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+ '@commons_io//jar',
':find-owners-lib',
':find-owners-prolog-rules',
],
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
index d7008b3..1b5bcd9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Config.java
@@ -32,16 +32,15 @@
static final String MAX_CACHE_AGE = "maxCacheAge"; // seconds to stay in cache
static final String MAX_CACHE_SIZE = "maxCacheSize"; // number of OwnersDb in cache
static final String MIN_OWNER_VOTE_LEVEL = "minOwnerVoteLevel"; // default +1
- static final String OWNERS_FILE_NAME = "ownersFileName"; // default "OWNERS"
+ static final String OWNERS = "OWNERS"; // Default file name
+ static final String OWNERS_FILE_NAME = "ownersFileName"; // config key for file name
+ static final String REJECT_ERROR_IN_OWNERS = "rejectErrorInOwners"; // config key for validator
static final String REPORT_SYNTAX_ERROR = "reportSyntaxError";
// Name of plugin and namespace.
static final String PLUGIN_NAME = "find-owners";
static final String PROLOG_NAMESPACE = "find_owners";
- // Default values.
- private static final String DEFAULT_OWNERS_FILE_NAME = "OWNERS";
-
// Global/plugin config parameters.
private static PluginConfigFactory config = null;
private static boolean addDebugMsg = false;
@@ -94,18 +93,18 @@
String name =
config
.getFromProjectConfigWithInheritance(project, PLUGIN_NAME)
- .getString(OWNERS_FILE_NAME, DEFAULT_OWNERS_FILE_NAME);
+ .getString(OWNERS_FILE_NAME, OWNERS);
if (name.trim().equals("")) {
log.error(
"Project " + project.get() + " has wrong " + OWNERS_FILE_NAME + ": \"" + name + "\"");
- return DEFAULT_OWNERS_FILE_NAME;
+ return OWNERS;
}
return name;
} catch (NoSuchProjectException e) {
log.error("Cannot find project: " + project);
}
}
- return DEFAULT_OWNERS_FILE_NAME;
+ return OWNERS;
}
@VisibleForTesting
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/Module.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/Module.java
index 9fc2709..e57bd51 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/findowners/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/Module.java
@@ -47,6 +47,7 @@
@Override
protected void configure() {
+ install(OwnersValidator.module());
install(
new RestApiModule() {
@Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
new file mode 100644
index 0000000..ce3b36e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/findowners/OwnersValidator.java
@@ -0,0 +1,359 @@
+// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
+
+import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS;
+import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS_FILE_NAME;
+import static com.googlesource.gerrit.plugins.findowners.Config.REJECT_ERROR_IN_OWNERS;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.config.ProjectConfigEntry;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+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;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+
+/** Check syntax of changed OWNERS files. */
+public class OwnersValidator implements CommitValidationListener {
+ private interface TreeWalkVisitor {
+ void onVisit(TreeWalk tw);
+ }
+
+ public static AbstractModule module() {
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), CommitValidationListener.class).to(OwnersValidator.class);
+ bind(ProjectConfigEntry.class)
+ .annotatedWith(Exports.named(REJECT_ERROR_IN_OWNERS))
+ .toInstance(
+ new ProjectConfigEntry(
+ "Reject OWNERS Files With Errors",
+ null,
+ ProjectConfigEntryType.BOOLEAN,
+ null,
+ false,
+ "Pushes of commits with errors in OWNERS files will be rejected."));
+ }
+ };
+ }
+
+ private final String pluginName;
+ private final PluginConfigFactory cfgFactory;
+ private final GitRepositoryManager repoManager;
+ private final Emails emails;
+
+ @Inject
+ OwnersValidator(
+ @PluginName String pluginName,
+ PluginConfigFactory cfgFactory,
+ GitRepositoryManager repoManager,
+ Emails emails) {
+ this.pluginName = pluginName;
+ this.cfgFactory = cfgFactory;
+ this.repoManager = repoManager;
+ this.emails = emails;
+ }
+
+ public static String getOwnersFileName(PluginConfig cfg) {
+ return getOwnersFileName(cfg, OWNERS);
+ }
+
+ public static String getOwnersFileName(PluginConfig cfg, String defaultName) {
+ return cfg.getString(OWNERS_FILE_NAME, defaultName);
+ }
+
+ public String getOwnersFileName(Project.NameKey project) {
+ String name = getOwnersFileName(cfgFactory.getFromGerritConfig(pluginName, true));
+ try {
+ return getOwnersFileName(
+ cfgFactory.getFromProjectConfigWithInheritance(project, pluginName), name);
+ } catch (NoSuchProjectException e) {
+ return name;
+ }
+ }
+
+ @VisibleForTesting
+ static boolean isActive(PluginConfig cfg) {
+ return cfg.getBoolean(REJECT_ERROR_IN_OWNERS, false);
+ }
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ List<CommitValidationMessage> messages = new LinkedList<>();
+ try {
+ Project.NameKey project = receiveEvent.project.getNameKey();
+ PluginConfig cfg = cfgFactory.getFromProjectConfigWithInheritance(project, pluginName);
+ if (isActive(cfg)) {
+ try (Repository repo = repoManager.openRepository(project)) {
+ String name = getOwnersFileName(project);
+ messages =
+ performValidation(repo, receiveEvent.commit, receiveEvent.revWalk, name, false);
+ }
+ }
+ } catch (NoSuchProjectException | IOException | ExecutionException e) {
+ throw new CommitValidationException("failed to check owners files", e);
+ }
+ if (hasError(messages)) {
+ throw new CommitValidationException("found invalid owners file", messages);
+ }
+ return messages;
+ }
+
+ @VisibleForTesting
+ List<CommitValidationMessage> performValidation(
+ Repository repo, RevCommit c, RevWalk revWalk, String ownersFileName, boolean verbose)
+ throws IOException, ExecutionException {
+ // Collect all messages from all files.
+ List<CommitValidationMessage> messages = new LinkedList<>();
+ // Collect all email addresses from all files and check each address only once.
+ Map<String, Set<String>> email2lines = new HashMap<>();
+ Map<String, ObjectId> content = getChangedOwners(repo, c, revWalk, ownersFileName);
+ for (String path : content.keySet()) {
+ ObjectLoader ol = revWalk.getObjectReader().open(content.get(path));
+ try (InputStream in = ol.openStream()) {
+ if (RawText.isBinary(in)) {
+ add(messages, path + " is a binary file", true); // OWNERS files cannot be binary
+ continue;
+ }
+ }
+ checkFile(messages, email2lines, path, ol, verbose);
+ }
+ checkEmails(messages, emails, email2lines, verbose);
+ return messages;
+ }
+
+ private static void checkEmails(
+ List<CommitValidationMessage> messages,
+ Emails emails,
+ Map<String, Set<String>> email2lines,
+ boolean verbose) {
+ List<String> owners = new ArrayList<>(email2lines.keySet());
+ if (verbose) {
+ for (String owner : owners) {
+ add(messages, "owner: " + owner, false);
+ }
+ }
+ if (emails == null || owners.isEmpty()) {
+ return;
+ }
+ String[] ownerEmailsAsArray = new String[owners.size()];
+ owners.toArray(ownerEmailsAsArray);
+ try {
+ Multimap<String, Account.Id> email2ids = emails.getAccountsFor(ownerEmailsAsArray);
+ for (String owner : ownerEmailsAsArray) {
+ boolean wrongEmail = (email2ids == null);
+ if (!wrongEmail) {
+ try {
+ Collection<Account.Id> ids = email2ids.get(owner);
+ wrongEmail = (ids == null || ids.size() != 1);
+ } catch (Exception e) {
+ wrongEmail = true;
+ }
+ }
+ if (wrongEmail) {
+ String locations = String.join(" ", email2lines.get(owner));
+ add(messages, "unknown: " + owner + " at " + locations, true);
+ }
+ }
+ } catch (Exception e) {
+ add(messages, "checkEmails failed.", true);
+ }
+ }
+
+ private static void checkFile(
+ List<CommitValidationMessage> messages,
+ Map<String, Set<String>> email2lines,
+ String path,
+ ObjectLoader ol,
+ boolean verbose)
+ throws IOException {
+ if (verbose) {
+ add(messages, "validate: " + path, false);
+ }
+ try (BufferedReader br =
+ new BufferedReader(new InputStreamReader(ol.openStream(), StandardCharsets.UTF_8))) {
+ int line = 0;
+ for (String l = br.readLine(); l != null; l = br.readLine()) {
+ line++;
+ checkLine(messages, email2lines, path, line, l, verbose);
+ }
+ }
+ }
+
+ // 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("*")) {
+ if (map.get(email) == null) {
+ map.put(email, new HashSet<>());
+ }
+ map.get(email).add(file + ":" + lineNumber);
+ }
+ }
+
+ private static boolean hasError(List<CommitValidationMessage> messages) {
+ for (CommitValidationMessage m : messages) {
+ if (m.isError()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void add(List<CommitValidationMessage> messages, String msg, boolean error) {
+ messages.add(new CommitValidationMessage(msg, error));
+ }
+
+ private static void checkLine(
+ List<CommitValidationMessage> messages,
+ Map<String, Set<String>> email2lines,
+ String path,
+ int lineNumber,
+ String line,
+ boolean verbose) {
+ 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);
+ } else {
+ String prefix = patFile.matcher(line).find() ? "ignored" : "syntax";
+ add(messages, prefix + ": " + path + ":" + lineNumber + ": " + line, true);
+ }
+ }
+
+ /**
+ * Find all changed OWNERS files which differ between the commit and its parents. Return a map
+ * from "Path to the changed file" to "ObjectId of the file".
+ */
+ private static Map<String, ObjectId> getChangedOwners(
+ Repository repo, RevCommit c, RevWalk revWalk, String ownersFileName) throws IOException {
+ final Map<String, ObjectId> content = new HashMap<>();
+ visitChangedEntries(
+ repo,
+ c,
+ revWalk,
+ new TreeWalkVisitor() {
+ @Override
+ public void onVisit(TreeWalk tw) {
+ if (isFile(tw) && ownersFileName.equals(tw.getNameString())) {
+ content.put(tw.getPathString(), tw.getObjectId(0));
+ }
+ }
+ });
+ return content;
+ }
+
+ private static boolean isFile(TreeWalk tw) {
+ return FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0))
+ || FileMode.REGULAR_FILE.equals(tw.getRawMode(0));
+ }
+
+ /**
+ * Find all TreeWalk entries which differ between the commit and its parents. If a TreeWalk entry
+ * is found this method calls the onVisit() method of the class TreeWalkVisitor.
+ */
+ private static void visitChangedEntries(
+ Repository repo, RevCommit c, RevWalk revWalk, TreeWalkVisitor visitor) throws IOException {
+ try (TreeWalk tw = new TreeWalk(revWalk.getObjectReader())) {
+ tw.setRecursive(true);
+ tw.setFilter(TreeFilter.ANY_DIFF);
+ tw.addTree(c.getTree());
+ if (c.getParentCount() > 0) {
+ for (RevCommit p : c.getParents()) {
+ if (p.getTree() == null) {
+ revWalk.parseHeaders(p);
+ }
+ tw.addTree(p.getTree());
+ }
+ while (tw.next()) {
+ if (isDifferentToAllParents(c, tw)) {
+ visitor.onVisit(tw);
+ }
+ }
+ } else {
+ while (tw.next()) {
+ visitor.onVisit(tw);
+ }
+ }
+ }
+ }
+
+ private static boolean isDifferentToAllParents(RevCommit c, TreeWalk tw) {
+ if (c.getParentCount() > 1) {
+ for (int p = 1; p <= c.getParentCount(); p++) {
+ if (tw.getObjectId(0).equals(tw.getObjectId(p))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 9e81e69..fa046cc 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -70,6 +70,16 @@
If a project has already used OWNERS files for other purpose,
the "ownersFileName" parameter can be used to change the default.
+## Validate OWNERS files before upload
+
+To check syntax of OWNERS files before they are uploaded,
+set the following variable in project.config files.
+
+```bash
+[plugin "find-owners"]
+ rejectErrorInOwners = true
+```
+
## Example 0, call `submit_filter/2`
The simplest configuration adds to `rules.pl` of the root
diff --git a/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java
new file mode 100644
index 0000000..dfbc0fa
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/findowners/OwnersValidatorTest.java
@@ -0,0 +1,314 @@
+// Copyright (C) 2017 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.googlesource.gerrit.plugins.findowners;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS;
+import static com.googlesource.gerrit.plugins.findowners.Config.OWNERS_FILE_NAME;
+import static com.googlesource.gerrit.plugins.findowners.Config.REJECT_ERROR_IN_OWNERS;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.api.AddCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Test OwnersValidator, which checks syntax of changed OWNERS files. */
+public class OwnersValidatorTest {
+
+ private class MockedEmails extends Emails {
+ Set<String> registered;
+
+ MockedEmails() {
+ super(null, null);
+ registered =
+ ImmutableSet.of(
+ "u1@g.com", "u2@g.com", "u2.m@g.com", "user1@google.com", "u1+review@g.com");
+ }
+
+ public ImmutableSetMultimap<String, Account.Id> getAccountsFor(String... emails) {
+ // Used by checkEmails; each email should have exactly one Account.Id
+ ImmutableSetMultimap.Builder<String, Account.Id> builder = ImmutableSetMultimap.builder();
+ int id = 1000000;
+ for (String s : registered) {
+ builder.put(s, new Account.Id(++id));
+ }
+ return builder.build();
+ }
+ }
+
+ private File repoFolder;
+ private Repository repo;
+
+ @Before
+ public void init() throws IOException {
+ repoFolder = File.createTempFile("Git", "");
+ repoFolder.delete();
+ repo = FileRepositoryBuilder.create(new File(repoFolder, ".git"));
+ repo.create();
+ }
+
+ @After
+ public void cleanup() throws IOException {
+ repo.close();
+ if (repoFolder.exists()) {
+ FileUtils.deleteDirectory(repoFolder);
+ }
+ }
+
+ private static final String OWNERS_ANDROID = "OWNERS.android"; // alternative OWNERS file name
+ private static final PluginConfig ANDROID_CONFIG = createAndroidConfig(); // use OWNERS_ANDROID
+ private static final PluginConfig EMPTY_CONFIG = new PluginConfig("", new Config());
+ private static final PluginConfig ENABLED_CONFIG = createEnabledConfig(); // use OWNERS
+ private static final PluginConfig DISABLED_CONFIG = createDisabledConfig();
+
+ @Test
+ public void chekIsActiveAndFileName() throws Exception {
+ // This check should be enabled in project.config, default is not active.
+ assertThat(OwnersValidator.isActive(EMPTY_CONFIG)).isFalse();
+ assertThat(OwnersValidator.isActive(ENABLED_CONFIG)).isTrue();
+ assertThat(OwnersValidator.isActive(ANDROID_CONFIG)).isTrue();
+ assertThat(OwnersValidator.isActive(DISABLED_CONFIG)).isFalse();
+ // Default file name is "OWNERS".
+ assertThat(OwnersValidator.getOwnersFileName(EMPTY_CONFIG)).isEqualTo(OWNERS);
+ assertThat(OwnersValidator.getOwnersFileName(ENABLED_CONFIG)).isEqualTo(OWNERS);
+ assertThat(OwnersValidator.getOwnersFileName(DISABLED_CONFIG)).isEqualTo(OWNERS);
+ assertThat(OwnersValidator.getOwnersFileName(ANDROID_CONFIG)).isEqualTo(OWNERS_ANDROID);
+ }
+
+ private static final Map<String, String> FILES_WITHOUT_OWNERS =
+ ImmutableMap.of("README", "any\n", "d1/test.c", "int x;\n");
+
+ @Test
+ public void testNoOwners() throws Exception {
+ try (RevWalk rw = new RevWalk(repo)) {
+ RevCommit c = makeCommit(rw, "Commit no OWNERS.", FILES_WITHOUT_OWNERS);
+ assertThat(validate(rw, c, false, ENABLED_CONFIG)).isEmpty();
+ assertThat(validate(rw, c, true, ENABLED_CONFIG)).isEmpty();
+ }
+ }
+
+ private static final Map<String, String> FILES_WITH_NO_ERROR =
+ ImmutableMap.of(
+ OWNERS,
+ "\n\n#comments ...\n ### more comments\n"
+ + " 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");
+
+ private static final Set<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");
+
+ @Test
+ public void testGoodInput() throws Exception {
+ try (RevWalk rw = new RevWalk(repo)) {
+ RevCommit c = makeCommit(rw, "Commit good files", FILES_WITH_NO_ERROR);
+ assertThat(validate(rw, c, false, ENABLED_CONFIG)).isEmpty();
+ assertThat(validate(rw, c, true, ENABLED_CONFIG))
+ .containsExactlyElementsIn(EXPECTED_VERBOSE_OUTPUT);
+ }
+ }
+
+ private static final Map<String, String> FILES_WITH_WRONG_SYNTAX =
+ ImmutableMap.of(
+ "README",
+ "# some content\nu2@g.com\n",
+ OWNERS,
+ "\nwrong syntax\n#comment\nuser1@google.com\n",
+ "d2/" + OWNERS,
+ "u1@g.com\nu3@g.com\n*\n",
+ "d3/" + OWNERS,
+ "\nfile: common/Owners\n");
+
+ private static final Set<String> EXPECTED_WRONG_SYNTAX =
+ ImmutableSet.of(
+ "ERROR: syntax: " + OWNERS + ":2: wrong syntax",
+ "ERROR: unknown: u3@g.com at d2/" + OWNERS + ":2",
+ "ERROR: ignored: d3/" + OWNERS + ":2: file: common/Owners");
+
+ private static final Set<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 + ":2: wrong syntax",
+ "ERROR: unknown: u3@g.com at d2/" + OWNERS + ":2",
+ "ERROR: ignored: d3/" + OWNERS + ":2: file: common/Owners");
+
+ @Test
+ public void testWrongSyntax() throws Exception {
+ try (RevWalk rw = new RevWalk(repo)) {
+ RevCommit c = makeCommit(rw, "Commit wrong syntax", FILES_WITH_WRONG_SYNTAX);
+ assertThat(validate(rw, c, false, ENABLED_CONFIG))
+ .containsExactlyElementsIn(EXPECTED_WRONG_SYNTAX);
+ assertThat(validate(rw, c, true, ENABLED_CONFIG))
+ .containsExactlyElementsIn(EXPECTED_VERBOSE_WRONG_SYNTAX);
+ }
+ }
+
+ private static final Map<String, String> FILES_WITH_WRONG_EMAILS =
+ ImmutableMap.of("d1/" + OWNERS, "u1@g.com\n", "d2/" + OWNERS_ANDROID, "u2@g.com\n");
+
+ private static final Set<String> EXPECTED_VERBOSE_DEFAULT =
+ ImmutableSet.of("MSG: validate: d1/" + OWNERS, "MSG: owner: u1@g.com");
+
+ private static final Set<String> EXPECTED_VERBOSE_ANDROID =
+ ImmutableSet.of("MSG: validate: d2/" + OWNERS_ANDROID, "MSG: owner: u2@g.com");
+
+ @Test
+ public void checkWrongEmails() throws Exception {
+ try (RevWalk rw = new RevWalk(repo)) {
+ RevCommit c = makeCommit(rw, "Commit Default", FILES_WITH_WRONG_EMAILS);
+ assertThat(validate(rw, c, true, ENABLED_CONFIG))
+ .containsExactlyElementsIn(EXPECTED_VERBOSE_DEFAULT);
+ }
+ }
+
+ @Test
+ public void checkAndroidOwners() throws Exception {
+ try (RevWalk rw = new RevWalk(repo)) {
+ RevCommit c = makeCommit(rw, "Commit Android", FILES_WITH_WRONG_EMAILS);
+ assertThat(validate(rw, c, true, ANDROID_CONFIG))
+ .containsExactlyElementsIn(EXPECTED_VERBOSE_ANDROID);
+ }
+ }
+
+ private static PluginConfig createEnabledConfig() {
+ PluginConfig c = new PluginConfig("", new Config());
+ c.setBoolean(REJECT_ERROR_IN_OWNERS, true);
+ return c;
+ }
+
+ private static PluginConfig createDisabledConfig() {
+ PluginConfig c = new PluginConfig("", new Config());
+ c.setBoolean(REJECT_ERROR_IN_OWNERS, false);
+ return c;
+ }
+
+ private static PluginConfig createAndroidConfig() {
+ PluginConfig c = createEnabledConfig();
+ c.setString(OWNERS_FILE_NAME, OWNERS_ANDROID);
+ return c;
+ }
+
+ private RevCommit makeCommit(RevWalk rw, String message, Map<String, String> fileStrings)
+ throws IOException, GitAPIException {
+ Map<File, byte[]> fileBytes = new HashMap<>();
+ for (String path : fileStrings.keySet()) {
+ fileBytes.put(
+ new File(repo.getDirectory().getParent(), path),
+ fileStrings.get(path).getBytes(StandardCharsets.UTF_8));
+ }
+ return makeCommit(rw, repo, message, fileBytes);
+ }
+
+ private List<String> validate(RevWalk rw, RevCommit c, boolean verbose, PluginConfig cfg)
+ throws Exception {
+ MockedEmails myEmails = new MockedEmails();
+ OwnersValidator validator = new OwnersValidator(null, null, null, myEmails);
+ String ownersFileName = OwnersValidator.getOwnersFileName(cfg);
+ List<CommitValidationMessage> m =
+ validator.performValidation(repo, c, rw, ownersFileName, verbose);
+ return transformMessages(m);
+ }
+
+ private static String generateFilePattern(File f, Git git) {
+ return f.getAbsolutePath()
+ .replace(git.getRepository().getWorkTree().getAbsolutePath(), "")
+ .substring(1);
+ }
+
+ private static void addFiles(Git git, Map<File, byte[]> files)
+ throws IOException, GitAPIException {
+ AddCommand ac = git.add();
+ for (File f : files.keySet()) {
+ if (!f.exists()) {
+ FileUtils.touch(f);
+ }
+ if (files.get(f) != null) {
+ FileUtils.writeByteArrayToFile(f, files.get(f));
+ }
+ ac = ac.addFilepattern(generateFilePattern(f, git));
+ }
+ ac.call();
+ }
+
+ private static RevCommit makeCommit(
+ RevWalk rw, Repository repo, String message, Map<File, byte[]> files)
+ throws IOException, GitAPIException {
+ try (Git git = new Git(repo)) {
+ if (files != null) {
+ addFiles(git, files);
+ }
+ return rw.parseCommit(git.commit().setMessage(message).call());
+ }
+ }
+
+ private static List<String> transformMessages(List<CommitValidationMessage> messages) {
+ return Lists.transform(
+ messages,
+ new Function<CommitValidationMessage, String>() {
+ @Override
+ public String apply(CommitValidationMessage input) {
+ String pre = (input.isError()) ? "ERROR: " : "MSG: ";
+ return pre + input.getMessage();
+ }
+ });
+ }
+
+ @Test
+ public void testTransformer() {
+ List<CommitValidationMessage> messages = new LinkedList<>();
+ messages.add(new CommitValidationMessage("a message", false));
+ messages.add(new CommitValidationMessage("an error", true));
+ Set<String> expected = ImmutableSet.of("ERROR: an error", "MSG: a message");
+ assertThat(transformMessages(messages)).containsExactlyElementsIn(expected);
+ }
+}