This is an automated email from the ASF dual-hosted git repository. pottlinger pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/creadur-rat.git
The following commit(s) were added to refs/heads/master by this push: new ddb409a8 RAT-335: Improve the parsing of gitignore files. new 4286326b Merge pull request #167 from nielsbasjes/RAT-335-GitIgnore ddb409a8 is described below commit ddb409a8d783518ac58c98e5b57a7b8b50083aa0 Author: Niels Basjes <ni...@basjes.nl> AuthorDate: Fri Nov 24 23:09:59 2023 +0100 RAT-335: Improve the parsing of gitignore files. --- apache-rat-plugin/pom.xml | 13 +++ .../java/org/apache/rat/mp/AbstractRatMojo.java | 93 +++++++++++++++------- .../org/apache/rat/mp/util/ExclusionHelper.java | 29 ++++--- .../org/apache/rat/mp/util/ScmIgnoreParser.java | 87 +++++--------------- .../rat/mp/util/ignore/GitIgnoreMatcher.java | 56 +++++++++++++ .../GlobIgnoreMatcher.java} | 90 ++++++++++++--------- .../apache/rat/mp/util/ignore/IgnoreMatcher.java | 37 +++++++++ .../mp/util/ignore/IgnoringDirectoryScanner.java | 74 +++++++++++++++++ .../java/org/apache/rat/mp/RatCheckMojoTest.java | 40 ++++++++++ .../apache/rat/mp/util/ExclusionHelperTest.java | 24 +++--- .../apache/rat/mp/util/ScmIgnoreParserTest.java | 10 ++- .../resources/unit/RAT-335-GitIgnore/.gitignore | 7 ++ .../resources/unit/RAT-335-GitIgnore/README.txt | 12 +++ .../unit/RAT-335-GitIgnore/dir1/.gitignore | 3 + .../resources/unit/RAT-335-GitIgnore/dir1/dir1.md | 1 + .../resources/unit/RAT-335-GitIgnore/dir1/dir1.txt | 1 + .../unit/RAT-335-GitIgnore/dir1/file1.log | 1 + .../resources/unit/RAT-335-GitIgnore/dir2/dir2.md | 1 + .../resources/unit/RAT-335-GitIgnore/dir2/dir2.txt | 1 + .../resources/unit/RAT-335-GitIgnore/dir3/dir3.log | 1 + .../unit/RAT-335-GitIgnore/dir3/file3.log | 1 + .../unit/RAT-335-GitIgnore/invoker.properties | 16 ++++ .../test/resources/unit/RAT-335-GitIgnore/pom.xml | 47 +++++++++++ .../test/resources/unit/RAT-335-GitIgnore/root.md | 1 + pom.xml | 4 + 25 files changed, 492 insertions(+), 158 deletions(-) diff --git a/apache-rat-plugin/pom.xml b/apache-rat-plugin/pom.xml index f7e07e5c..7d602e72 100644 --- a/apache-rat-plugin/pom.xml +++ b/apache-rat-plugin/pom.xml @@ -89,6 +89,8 @@ <exclude>src/test/resources/unit/it2/src.txt</exclude> <exclude>src/test/resources/unit/it3/src.apt</exclude> <exclude>src/test/resources/unit/it4/*.html</exclude> + <exclude>src/test/resources/unit/RAT-335-GitIgnore/*</exclude> + <exclude>src/test/resources/unit/RAT-335-GitIgnore/**/*</exclude> <exclude>**/*.iml</exclude> <!-- RAT-171: needs to be added since SCM ignores are only parsed in project root --> <exclude>**/.bzrignore</exclude> @@ -287,6 +289,17 @@ <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-simple</artifactId> + <version>1.7.36</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>nl.basjes.gitignore</groupId> + <artifactId>gitignore-reader</artifactId> + <version>1.2.1</version> + </dependency> </dependencies> <reporting> <plugins> diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/AbstractRatMojo.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/AbstractRatMojo.java index 11ccd836..89aaa30f 100644 --- a/apache-rat-plugin/src/main/java/org/apache/rat/mp/AbstractRatMojo.java +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/AbstractRatMojo.java @@ -38,9 +38,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.SortedSet; import java.util.function.Consumer; @@ -53,16 +51,17 @@ import org.apache.maven.project.MavenProject; import org.apache.rat.ConfigurationException; import org.apache.rat.Defaults; import org.apache.rat.ReportConfiguration; -import org.apache.rat.config.AddLicenseHeaders; import org.apache.rat.config.SourceCodeManagementSystems; import org.apache.rat.configuration.Format; import org.apache.rat.configuration.LicenseReader; import org.apache.rat.configuration.MatcherReader; -import org.apache.rat.configuration.MatcherBuilderTracker; import org.apache.rat.license.ILicense; import org.apache.rat.license.ILicenseFamily; import org.apache.rat.license.LicenseSetFactory.LicenseFilter; import org.apache.rat.mp.util.ScmIgnoreParser; +import org.apache.rat.mp.util.ignore.GlobIgnoreMatcher; +import org.apache.rat.mp.util.ignore.IgnoreMatcher; +import org.apache.rat.mp.util.ignore.IgnoringDirectoryScanner; import org.apache.rat.report.IReportable; import org.codehaus.plexus.util.DirectoryScanner; @@ -335,7 +334,7 @@ public abstract class AbstractRatMojo extends AbstractMojo { * UndeclaredThrowableExceptions. */ private IReportable getReportable() throws MojoExecutionException { - final DirectoryScanner ds = new DirectoryScanner(); + final IgnoringDirectoryScanner ds = new IgnoringDirectoryScanner(); ds.setBasedir(basedir); setExcludes(ds); setIncludes(ds); @@ -476,8 +475,8 @@ public abstract class AbstractRatMojo extends AbstractMojo { return patterns; } - private void setExcludes(DirectoryScanner ds) throws MojoExecutionException { - final List<String> excludeList = mergeDefaultExclusions(); + private void setExcludes(IgnoringDirectoryScanner ds) throws MojoExecutionException { + final List<IgnoreMatcher> ignoreMatchers = mergeDefaultExclusions(); if (excludes == null || excludes.length == 0) { getLog().debug("No excludes explicitly specified."); } else { @@ -486,47 +485,77 @@ public abstract class AbstractRatMojo extends AbstractMojo { getLog().debug("Exclude: " + exclude); } } + + final List<String> globExcludes = new ArrayList<>(); + for (IgnoreMatcher ignoreMatcher : ignoreMatchers) { + if (ignoreMatcher instanceof GlobIgnoreMatcher) { + // The glob matching we do via the DirectoryScanner + globExcludes.addAll(((GlobIgnoreMatcher) ignoreMatcher).getExclusionLines()); + } else { + // All others (git) are used directly + ds.addIgnoreMatcher(ignoreMatcher); + } + } + if (excludes != null) { - Collections.addAll(excludeList, excludes); + Collections.addAll(globExcludes, excludes); } - if (!excludeList.isEmpty()) { - final String[] allExcludes = excludeList.toArray(new String[excludeList.size()]); + if (!globExcludes.isEmpty()) { + final String[] allExcludes = globExcludes.toArray(new String[globExcludes.size()]); ds.setExcludes(allExcludes); } } - private List<String> mergeDefaultExclusions() throws MojoExecutionException { - final Set<String> results = new HashSet<>(); + private List<IgnoreMatcher> mergeDefaultExclusions() throws MojoExecutionException { + List<IgnoreMatcher> ignoreMatchers = new ArrayList<>(); + + final GlobIgnoreMatcher basicRules = new GlobIgnoreMatcher(); - addPlexusAndScmDefaults(getLog(), useDefaultExcludes, results); - addMavenDefaults(getLog(), useMavenDefaultExcludes, results); - addEclipseDefaults(getLog(), useEclipseDefaultExcludes, results); - addIdeaDefaults(getLog(), useIdeaDefaultExcludes, results); + basicRules.addRules(addPlexusAndScmDefaults(getLog(), useDefaultExcludes)); + basicRules.addRules(addMavenDefaults(getLog(), useMavenDefaultExcludes)); + basicRules.addRules(addEclipseDefaults(getLog(), useEclipseDefaultExcludes)); + basicRules.addRules(addIdeaDefaults(getLog(), useIdeaDefaultExcludes)); if (parseSCMIgnoresAsExcludes) { getLog().debug("Will parse SCM ignores for exclusions..."); - results.addAll(ScmIgnoreParser.getExclusionsFromSCM(getLog(), project.getBasedir())); + ignoreMatchers.addAll(ScmIgnoreParser.getExclusionsFromSCM(getLog(), project.getBasedir())); getLog().debug("Finished adding exclusions from SCM ignore files."); } if (excludeSubProjects && project != null && project.getModules() != null) { - for (final Object o : project.getModules()) { - final String moduleSubPath = (String) o; + for (final String moduleSubPath : project.getModules()) { if (new File(basedir, moduleSubPath).isDirectory()) { - results.add(moduleSubPath + "/**/*"); + basicRules.addRule(moduleSubPath + "/**/*"); } else { - results.add(StringUtils.substringBeforeLast(moduleSubPath, "/") + "/**/*"); + basicRules.addRule(StringUtils.substringBeforeLast(moduleSubPath, "/") + "/**/*"); } } } - getLog().debug("Finished creating list of implicit excludes."); - if (results.isEmpty()) { - getLog().debug("No excludes implicitly specified."); - } else { - getLog().debug(results.size() + " implicit excludes."); - for (final String exclude : results) { - getLog().debug("Implicit exclude: " + exclude); + if (getLog().isDebugEnabled()) { + getLog().debug("Finished creating list of implicit excludes."); + if (basicRules.getExclusionLines().isEmpty() && ignoreMatchers.isEmpty()) { + getLog().debug("No excludes implicitly specified."); + } else { + if (!basicRules.getExclusionLines().isEmpty()) { + getLog().debug(basicRules.getExclusionLines().size() + " implicit excludes."); + for (final String exclude : basicRules.getExclusionLines()) { + getLog().debug("Implicit exclude: " + exclude); + } + } + for (IgnoreMatcher ignoreMatcher : ignoreMatchers) { + if (ignoreMatcher instanceof GlobIgnoreMatcher) { + GlobIgnoreMatcher globIgnoreMatcher = (GlobIgnoreMatcher) ignoreMatcher; + if (!globIgnoreMatcher.getExclusionLines().isEmpty()) { + getLog().debug(globIgnoreMatcher.getExclusionLines().size() + " implicit excludes from SCM."); + for (final String exclude : globIgnoreMatcher.getExclusionLines()) { + getLog().debug("Implicit exclude: " + exclude); + } + } + } else { + getLog().debug("Implicit exclude: \n" + ignoreMatcher); + } + } } } if (excludesFile != null) { @@ -539,9 +568,13 @@ public abstract class AbstractRatMojo extends AbstractMojo { } final String charset = excludesFileCharset == null ? "UTF8" : excludesFileCharset; getLog().debug("Loading excludes from file " + f + ", using character set " + charset); - results.addAll(getPatternsFromFile(f, charset)); + basicRules.addRules(getPatternsFromFile(f, charset)); + } + + if (!basicRules.isEmpty()) { + ignoreMatchers.add(basicRules); } - return new ArrayList<>(results); + return ignoreMatchers; } } diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ExclusionHelper.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ExclusionHelper.java index 0ab75e90..8558e16b 100644 --- a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ExclusionHelper.java +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ExclusionHelper.java @@ -2,13 +2,17 @@ package org.apache.rat.mp.util; import org.apache.maven.plugin.logging.Log; import org.apache.rat.config.SourceCodeManagementSystems; +import org.codehaus.plexus.util.AbstractScanner; import org.codehaus.plexus.util.DirectoryScanner; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; +import static org.codehaus.plexus.util.AbstractScanner.DEFAULTEXCLUDES; + /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -68,22 +72,22 @@ public final class ExclusionHelper { "*.iws", // ".idea/**/*")); - public static void addPlexusAndScmDefaults(Log log, final boolean useDefaultExcludes, - final Set<String> excludeList1) { + public static Set<String> addPlexusAndScmDefaults(Log log, final boolean useDefaultExcludes) { + Set<String> excludeList = new HashSet<>(); if (useDefaultExcludes) { log.debug("Adding plexus default exclusions..."); - Collections.addAll(excludeList1, DirectoryScanner.DEFAULTEXCLUDES); + Collections.addAll(excludeList, DEFAULTEXCLUDES); log.debug("Adding SCM default exclusions..."); - excludeList1.addAll(// - SourceCodeManagementSystems.getPluginExclusions()); + excludeList.addAll(SourceCodeManagementSystems.getPluginExclusions()); } else { log.debug("rat.useDefaultExcludes set to false. " + "Plexus and SCM default exclusions will not be added"); } + return excludeList; } - public static void addMavenDefaults(Log log, boolean useMavenDefaultExcludes, - final Set<String> excludeList) { + public static Set<String> addMavenDefaults(Log log, boolean useMavenDefaultExcludes) { + Set<String> excludeList = new HashSet<>(); if (useMavenDefaultExcludes) { log.debug("Adding exclusions often needed by Maven projects..."); excludeList.addAll(MAVEN_DEFAULT_EXCLUDES); @@ -91,10 +95,11 @@ public final class ExclusionHelper { log.debug("rat.useMavenDefaultExcludes set to false. " + "Exclusions often needed by Maven projects will not be added."); } + return excludeList; } - public static void addEclipseDefaults(Log log, boolean useEclipseDefaultExcludes, - final Set<String> excludeList) { + public static Set<String> addEclipseDefaults(Log log, boolean useEclipseDefaultExcludes) { + Set<String> excludeList = new HashSet<>(); if (useEclipseDefaultExcludes) { log.debug("Adding exclusions often needed by projects " + "developed in Eclipse..."); @@ -104,10 +109,11 @@ public final class ExclusionHelper { + "Exclusions often needed by projects developed in " + "Eclipse will not be added."); } + return excludeList; } - public static void addIdeaDefaults(Log log, boolean useIdeaDefaultExcludes, - final Set<String> excludeList) { + public static Set<String> addIdeaDefaults(Log log, boolean useIdeaDefaultExcludes) { + Set<String> excludeList = new HashSet<>(); if (useIdeaDefaultExcludes) { log.debug("Adding exclusions often needed by projects " + "developed in IDEA..."); @@ -117,6 +123,7 @@ public final class ExclusionHelper { + "Exclusions often needed by projects developed in " + "IDEA will not be added."); } + return excludeList; } } diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ScmIgnoreParser.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ScmIgnoreParser.java index a165454f..675a3644 100644 --- a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ScmIgnoreParser.java +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ScmIgnoreParser.java @@ -20,16 +20,14 @@ package org.apache.rat.mp.util; */ -import org.apache.commons.io.IOUtils; import org.apache.maven.plugin.logging.Log; import org.apache.rat.config.SourceCodeManagementSystems; +import org.apache.rat.mp.util.ignore.GitIgnoreMatcher; +import org.apache.rat.mp.util.ignore.GlobIgnoreMatcher; +import org.apache.rat.mp.util.ignore.IgnoreMatcher; -import java.io.BufferedReader; import java.io.File; -import java.io.FileReader; -import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -41,41 +39,6 @@ public final class ScmIgnoreParser { // prevent instantiation of utility class } - private static final List<String> COMMENT_PREFIXES = Arrays.asList("#", "##", "//", "/**", "/*"); - - /** - * Parses excludes from the given SCM ignore file. - * - * @param log Maven log to show output during RAT runs. - * @param scmIgnore if <code>null</code> or invalid an empty list of exclusions is returned. - * @return all exclusions (=non-comment lines) from the SCM ignore file. - */ - public static List<String> getExcludesFromFile(final Log log, final File scmIgnore) { - - final List<String> exclusionLines = new ArrayList<>(); - - if (scmIgnore != null && scmIgnore.exists() && scmIgnore.isFile()) { - log.debug("Parsing exclusions from " + scmIgnore); - BufferedReader reader = null; - try { - reader = new BufferedReader(new FileReader(scmIgnore)); - String line; - while ((line = reader.readLine()) != null) { - if (!isComment(line)) { - exclusionLines.add(line); - log.debug("Added " + line); - } - } - } catch (final IOException e) { - log.warn("Cannot parse " + scmIgnore + " for exclusions. Will skip this file."); - log.debug("Skip parsing " + scmIgnore + " due to " + e.getMessage()); - } finally { - IOUtils.closeQuietly(reader); - } - } - return exclusionLines; - } - /** * Parse ignore files from all known SCMs that have ignore files. * @@ -83,36 +46,28 @@ public final class ScmIgnoreParser { * @param baseDir base directory from which to look for SCM ignores. * @return Exclusions from the SCM ignore files. */ - public static List<String> getExclusionsFromSCM(final Log log, final File baseDir) { - List<String> exclusions = new ArrayList<>(); + public static List<IgnoreMatcher> getExclusionsFromSCM(final Log log, final File baseDir) { + List<IgnoreMatcher> ignoreMatchers = new ArrayList<>(); for (SourceCodeManagementSystems scm : SourceCodeManagementSystems.values()) { - if (scm.hasIgnoreFile()) { - exclusions.addAll(getExcludesFromFile(log, new File(baseDir, scm.getIgnoreFile()))); + switch (scm) { + case GIT: + GitIgnoreMatcher gitIgnoreMatcher = new GitIgnoreMatcher(log, baseDir); + if (!gitIgnoreMatcher.isEmpty()) { + ignoreMatchers.add(gitIgnoreMatcher); + } + break; + default: + if (scm.hasIgnoreFile()) { + GlobIgnoreMatcher ignoreMatcher = new GlobIgnoreMatcher(log, new File(baseDir, scm.getIgnoreFile())); + if (!ignoreMatcher.isEmpty()) { + ignoreMatchers.add(ignoreMatcher); + } + } + break; } } - return exclusions; - + return ignoreMatchers; } - /** - * Determines whether the given line is a comment or not based on scanning - * for prefixes - * {@see COMMENT_PREFIXES}. - * - * @param line line to verify. - * @return <code>true</code> if the given line is a commented out line. - */ - static boolean isComment(final String line) { - if (line == null || line.length() <= 0) { - return false; - } - final String trimLine = line.trim(); - for (String prefix : COMMENT_PREFIXES) { - if (trimLine.startsWith(prefix)) { - return true; - } - } - return false; - } } diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/GitIgnoreMatcher.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/GitIgnoreMatcher.java new file mode 100644 index 00000000..844ce90c --- /dev/null +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/GitIgnoreMatcher.java @@ -0,0 +1,56 @@ +package org.apache.rat.mp.util.ignore; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import nl.basjes.gitignore.GitIgnoreFileSet; +import org.apache.maven.plugin.logging.Log; + +import java.io.File; +import java.util.Optional; + +public class GitIgnoreMatcher implements IgnoreMatcher { + + private final GitIgnoreFileSet gitIgnoreFileSet; + + public GitIgnoreMatcher(final Log log, final File projectBaseDir) { + log.debug("Recursively loading .gitignore files in " + projectBaseDir); + // This will walk the project tree and load all .gitignore files + gitIgnoreFileSet = new GitIgnoreFileSet(projectBaseDir); + } + + @Override + public boolean isEmpty() { + return gitIgnoreFileSet.isEmpty(); + } + + @Override + public Optional<Boolean> isIgnoredFile(String filename) { + Boolean isIgnoredFile = gitIgnoreFileSet.isIgnoredFile(filename); + if (isIgnoredFile == null) { + return Optional.empty(); + } + return Optional.of(isIgnoredFile); + } + + @Override + public String toString() { + return "Loaded .gitignore data:\n" + gitIgnoreFileSet; + } +} diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ScmIgnoreParser.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/GlobIgnoreMatcher.java similarity index 67% copy from apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ScmIgnoreParser.java copy to apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/GlobIgnoreMatcher.java index a165454f..3df95cd3 100644 --- a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ScmIgnoreParser.java +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/GlobIgnoreMatcher.java @@ -1,4 +1,4 @@ -package org.apache.rat.mp.util; +package org.apache.rat.mp.util.ignore; /* * Licensed to the Apache Software Foundation (ASF) under one @@ -19,10 +19,8 @@ package org.apache.rat.mp.util; * under the License. */ - import org.apache.commons.io.IOUtils; import org.apache.maven.plugin.logging.Log; -import org.apache.rat.config.SourceCodeManagementSystems; import java.io.BufferedReader; import java.io.File; @@ -30,30 +28,53 @@ import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Optional; -/** - * Helper to parse SCM ignore files to add entries as excludes during RAT runs. - * Since we log errors it needs to reside inside of the maven plugin. - */ -public final class ScmIgnoreParser { - private ScmIgnoreParser() { - // prevent instantiation of utility class - } +public class GlobIgnoreMatcher implements IgnoreMatcher { + + final List<String> exclusionLines = new ArrayList<>(); private static final List<String> COMMENT_PREFIXES = Arrays.asList("#", "##", "//", "/**", "/*"); + public GlobIgnoreMatcher() { + // No rules to load + } + + public GlobIgnoreMatcher(final Log log, final File scmIgnore) { + loadFile(log, scmIgnore); + } + + /** + * Add a single rule to the set + * @param rule The line that matches some files + */ + public void addRule(final String rule) { + if (!exclusionLines.contains(rule)) { + exclusionLines.add(rule); + } + } + + /** + * Add a set of rules to the set + * @param rules The line that matches some files + */ + public void addRules(final Collection<String> rules) { + for (String rule : rules) { + if (!exclusionLines.contains(rule)) { + exclusionLines.add(rule); + } + } + } + /** * Parses excludes from the given SCM ignore file. * * @param log Maven log to show output during RAT runs. * @param scmIgnore if <code>null</code> or invalid an empty list of exclusions is returned. - * @return all exclusions (=non-comment lines) from the SCM ignore file. */ - public static List<String> getExcludesFromFile(final Log log, final File scmIgnore) { - - final List<String> exclusionLines = new ArrayList<>(); - + public void loadFile(final Log log, final File scmIgnore) { if (scmIgnore != null && scmIgnore.exists() && scmIgnore.isFile()) { log.debug("Parsing exclusions from " + scmIgnore); BufferedReader reader = null; @@ -73,25 +94,6 @@ public final class ScmIgnoreParser { IOUtils.closeQuietly(reader); } } - return exclusionLines; - } - - /** - * Parse ignore files from all known SCMs that have ignore files. - * - * @param log Show information via maven logger. - * @param baseDir base directory from which to look for SCM ignores. - * @return Exclusions from the SCM ignore files. - */ - public static List<String> getExclusionsFromSCM(final Log log, final File baseDir) { - List<String> exclusions = new ArrayList<>(); - for (SourceCodeManagementSystems scm : SourceCodeManagementSystems.values()) { - if (scm.hasIgnoreFile()) { - exclusions.addAll(getExcludesFromFile(log, new File(baseDir, scm.getIgnoreFile()))); - } - } - return exclusions; - } /** @@ -102,7 +104,7 @@ public final class ScmIgnoreParser { * @param line line to verify. * @return <code>true</code> if the given line is a commented out line. */ - static boolean isComment(final String line) { + public static boolean isComment(final String line) { if (line == null || line.length() <= 0) { return false; } @@ -115,4 +117,20 @@ public final class ScmIgnoreParser { } return false; } + + public List<String> getExclusionLines() { + return exclusionLines; + } + + @Override + public boolean isEmpty() { + return exclusionLines.isEmpty(); + } + + @Override + public Optional<Boolean> isIgnoredFile(String filename) { + // Not used for Glob Rules; using the DirectoryScanner instead + // It CAN be moved here if so desired. + return Optional.empty() ; + } } diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/IgnoreMatcher.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/IgnoreMatcher.java new file mode 100644 index 00000000..c1d1f49f --- /dev/null +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/IgnoreMatcher.java @@ -0,0 +1,37 @@ +package org.apache.rat.mp.util.ignore; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import java.util.Optional; + +public interface IgnoreMatcher { + /** + * Checks if the file matches the stored expressions. + * @param filename The filename to be checked + * @return empty: not matched, True: must be ignored, False: it must be UNignored + */ + Optional<Boolean> isIgnoredFile(String filename); + + /** + * Returns {@code true} if this IgnoreMatcher contains no rules. + * @return {@code true} if this IgnoreMatcher contains no rules + */ + boolean isEmpty(); +} diff --git a/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/IgnoringDirectoryScanner.java b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/IgnoringDirectoryScanner.java new file mode 100644 index 00000000..d8e340a4 --- /dev/null +++ b/apache-rat-plugin/src/main/java/org/apache/rat/mp/util/ignore/IgnoringDirectoryScanner.java @@ -0,0 +1,74 @@ +package org.apache.rat.mp.util.ignore; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import org.codehaus.plexus.util.DirectoryScanner; + +import java.util.ArrayList; +import java.util.List; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +public class IgnoringDirectoryScanner extends DirectoryScanner { + + public IgnoringDirectoryScanner() { + super(); + } + + List<IgnoreMatcher> ignoreMatcherList = new ArrayList<>(); + + public void addIgnoreMatcher(IgnoreMatcher ignoreMatcher) { + ignoreMatcherList.add(ignoreMatcher); + } + + private boolean matchesAnIgnoreMatcher(String name) { + for (IgnoreMatcher ignoreMatcher : ignoreMatcherList) { + if (ignoreMatcher.isIgnoredFile(name).orElse(FALSE) == TRUE) { + return true; + } + } + return false; + } + + @Override + protected boolean isExcluded(String name) { + if (matchesAnIgnoreMatcher(name)) { + return true; + } + return super.isExcluded(name); + } + + @Override + protected boolean isExcluded(String name, String[] tokenizedName) { + if (matchesAnIgnoreMatcher(name)) { + return true; + } + return super.isExcluded(name, tokenizedName); + } + + @Override + protected boolean isExcluded(String name, char[][] tokenizedName) { + if (matchesAnIgnoreMatcher(name)) { + return true; + } + return super.isExcluded(name, tokenizedName); + } +} diff --git a/apache-rat-plugin/src/test/java/org/apache/rat/mp/RatCheckMojoTest.java b/apache-rat-plugin/src/test/java/org/apache/rat/mp/RatCheckMojoTest.java index d97b6786..bac6a379 100644 --- a/apache-rat-plugin/src/test/java/org/apache/rat/mp/RatCheckMojoTest.java +++ b/apache-rat-plugin/src/test/java/org/apache/rat/mp/RatCheckMojoTest.java @@ -16,6 +16,7 @@ */ package org.apache.rat.mp; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.rat.mp.RatTestHelpers.ensureRatReportIsCorrect; import static org.apache.rat.mp.RatTestHelpers.getSourceDirectory; import static org.apache.rat.mp.RatTestHelpers.newArtifactFactory; @@ -23,7 +24,11 @@ import static org.apache.rat.mp.RatTestHelpers.newArtifactRepository; import static org.apache.rat.mp.RatTestHelpers.newSiteRenderer; import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import org.apache.commons.io.FileUtils; import org.apache.rat.ReportConfiguration; import org.apache.rat.ReportConfigurationTest; import org.apache.rat.license.ILicenseFamily; @@ -75,6 +80,7 @@ public class RatCheckMojoTest extends BetterAbstractMojoTestCase { setVariableValueToObject(mojo, "siteRenderer", newSiteRenderer(getContainer())); } else if (mojo instanceof RatCheckMojo) { final File ratTxtFile = new File(buildDirectory, "rat.txt"); + FileUtils.write(ratTxtFile, "", UTF_8); // Ensure the output file exists and is empty (rerunning the test will append) setVariableValueToObject(mojo, "reportFile", ratTxtFile); } return mojo; @@ -184,4 +190,38 @@ public class RatCheckMojoTest extends BetterAbstractMojoTestCase { ensureRatReportIsCorrect(ratTxtFile, expected, TextUtils.EMPTY); } + + /** + * Tests verifying gitignore parsing + */ + public void testRAT335GitIgnore() throws Exception { + final RatCheckMojo mojo = newRatCheckMojo("RAT-335-GitIgnore"); + final File ratTxtFile = getRatTxtFile(mojo); + final String dir = getDir(mojo); + final String[] expected = { + "Notes: 1", + "Binaries: 0", + "Archives: 0", + "Standards: 5$", + "Apache Licensed: 2$", + "Generated Documents: 0", + "^3 Unknown Licenses", + " AL +\\Q" + dir + "pom.xml\\E$", + "\\Q!????? " + dir + "dir1/dir1.md\\E$", + "\\Q!????? " + dir + "dir2/dir2.txt\\E$", + "\\Q!????? " + dir + "dir3/file3.log\\E$", + "^== File: \\Q" + dir + "dir1/dir1.md\\E$", + "^== File: \\Q" + dir + "dir2/dir2.txt\\E$", + "^== File: \\Q" + dir + "dir3/file3.log\\E$" + }; + try { + mojo.execute(); + fail("Expected RatCheckException"); + } catch (RatCheckException e) { + final String msg = e.getMessage(); + assertTrue("report filename was not contained in '" + msg + "'", msg.contains(ratTxtFile.getName())); + assertFalse("no null allowed in '" + msg + "'", (msg.toUpperCase().contains("NULL"))); + ensureRatReportIsCorrect(ratTxtFile, expected, TextUtils.EMPTY); + } + } } diff --git a/apache-rat-plugin/src/test/java/org/apache/rat/mp/util/ExclusionHelperTest.java b/apache-rat-plugin/src/test/java/org/apache/rat/mp/util/ExclusionHelperTest.java index ff31723b..1af2b9ae 100644 --- a/apache-rat-plugin/src/test/java/org/apache/rat/mp/util/ExclusionHelperTest.java +++ b/apache-rat-plugin/src/test/java/org/apache/rat/mp/util/ExclusionHelperTest.java @@ -56,33 +56,33 @@ public class ExclusionHelperTest { @Test public void testAddingEclipseExclusions() { final Set<String> exclusion = new HashSet<>(); - addEclipseDefaults(log, false, exclusion); + exclusion.addAll(addEclipseDefaults(log, false)); assertTrue(exclusion.isEmpty()); - addEclipseDefaults(log, true, exclusion); + exclusion.addAll(addEclipseDefaults(log, true)); assertEquals(5, exclusion.size()); - addEclipseDefaults(log, true, exclusion); + exclusion.addAll(addEclipseDefaults(log, true)); assertEquals(5, exclusion.size()); } @Test public void testAddingIdeaExclusions() { final Set<String> exclusion = new HashSet<>(); - addIdeaDefaults(log, false, exclusion); + exclusion.addAll(addIdeaDefaults(log, false)); assertTrue(exclusion.isEmpty()); - addIdeaDefaults(log, true, exclusion); + exclusion.addAll(addIdeaDefaults(log, true)); assertEquals(4, exclusion.size()); - addIdeaDefaults(log, true, exclusion); + exclusion.addAll(addIdeaDefaults(log, true)); assertEquals(4, exclusion.size()); } @Test public void testAddingMavenExclusions() { final Set<String> exclusion = new HashSet<>(); - addMavenDefaults(log, false, exclusion); + exclusion.addAll(addMavenDefaults(log, false)); assertTrue(exclusion.isEmpty()); - addMavenDefaults(log, true, exclusion); + exclusion.addAll(addMavenDefaults(log, true)); assertEquals(8, exclusion.size()); - addMavenDefaults(log, true, exclusion); + exclusion.addAll(addMavenDefaults(log, true)); assertEquals(8, exclusion.size()); } @@ -91,14 +91,14 @@ public class ExclusionHelperTest { final int expectedSizeMergedFromPlexusDefaultsAndScm = (37 + SourceCodeManagementSystems.getPluginExclusions().size()); final Set<String> exclusion = new HashSet<>(); - addPlexusAndScmDefaults(log, false, exclusion); + exclusion.addAll(addPlexusAndScmDefaults(log, false)); assertTrue(exclusion.isEmpty()); - addPlexusAndScmDefaults(log, true, exclusion); + exclusion.addAll(addPlexusAndScmDefaults(log, true)); assertEquals( "Did you upgrade plexus to get more default excludes?",// expectedSizeMergedFromPlexusDefaultsAndScm,// exclusion.size()); - addPlexusAndScmDefaults(log, true, exclusion); + exclusion.addAll(addPlexusAndScmDefaults(log, true)); assertEquals( "Did you upgrade plexus to get more default excludes?",// expectedSizeMergedFromPlexusDefaultsAndScm,// diff --git a/apache-rat-plugin/src/test/java/org/apache/rat/mp/util/ScmIgnoreParserTest.java b/apache-rat-plugin/src/test/java/org/apache/rat/mp/util/ScmIgnoreParserTest.java index 67846108..342380dc 100644 --- a/apache-rat-plugin/src/test/java/org/apache/rat/mp/util/ScmIgnoreParserTest.java +++ b/apache-rat-plugin/src/test/java/org/apache/rat/mp/util/ScmIgnoreParserTest.java @@ -18,6 +18,7 @@ package org.apache.rat.mp.util; import org.apache.commons.io.IOUtils; import org.apache.maven.plugin.logging.Log; +import org.apache.rat.mp.util.ignore.GlobIgnoreMatcher; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -31,9 +32,8 @@ import java.io.FileWriter; import java.io.IOException; import java.util.List; -import static org.apache.rat.mp.util.ScmIgnoreParser.getExcludesFromFile; import static org.apache.rat.mp.util.ScmIgnoreParser.getExclusionsFromSCM; -import static org.apache.rat.mp.util.ScmIgnoreParser.isComment; +import static org.apache.rat.mp.util.ignore.GlobIgnoreMatcher.isComment; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -67,6 +67,9 @@ public class ScmIgnoreParserTest { assertTrue(isComment(" /* comment that is */ ")); } + public List<String> getExcludesFromFile(final Log log, final File scmIgnore) { + return new GlobIgnoreMatcher(log, scmIgnore).getExclusionLines(); + } @Test public void parseFromNonExistingFileOrDirectoryOrNull() { @@ -77,7 +80,8 @@ public class ScmIgnoreParserTest { @Test public void parseFromTargetDirectoryHopefullyWithoutSCMIgnores() { - assertTrue(getExclusionsFromSCM(log, new File("./target")).isEmpty()); + // The target directory contains ignore files from other tests + assertTrue(getExclusionsFromSCM(log, new File("./target/classes")).isEmpty()); } @Test diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/.gitignore b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/.gitignore new file mode 100644 index 00000000..8855fa80 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/.gitignore @@ -0,0 +1,7 @@ +*.md + +# This makes it ignore dir3/dir3.log and dir3/file3.log +*.log + +# This makes it "unignore" dir3/file3.log +!file*.log diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/README.txt b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/README.txt new file mode 100644 index 00000000..8681fb58 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/README.txt @@ -0,0 +1,12 @@ +Note the output when running in the real commandline version of git + +# Files that must be ignored (dropping the gitignore matches outside of this test tree) +$ git check-ignore --no-index --verbose $(find . -type f|sort) | fgrep 'apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/' + +apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/.gitignore:2:!dir1.md ./dir1/dir1.md +apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/.gitignore:1:*.txt ./dir1/dir1.txt +apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/.gitignore:3:file1.log ./dir1/file1.log +apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/.gitignore:1:*.md ./dir2/dir2.md +apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/.gitignore:4:*.log ./dir3/dir3.log +apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/.gitignore:7:!file*.log ./dir3/file3.log +apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/.gitignore:1:*.md ./root.md diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/.gitignore b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/.gitignore new file mode 100644 index 00000000..26fd5c95 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/.gitignore @@ -0,0 +1,3 @@ +*.txt +!dir1.md +file1.log \ No newline at end of file diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/dir1.md b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/dir1.md new file mode 100644 index 00000000..a31cbc89 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/dir1.md @@ -0,0 +1 @@ +File without a valid license diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/dir1.txt b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/dir1.txt new file mode 100644 index 00000000..a31cbc89 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/dir1.txt @@ -0,0 +1 @@ +File without a valid license diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/file1.log b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/file1.log new file mode 100644 index 00000000..a31cbc89 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir1/file1.log @@ -0,0 +1 @@ +File without a valid license diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir2/dir2.md b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir2/dir2.md new file mode 100644 index 00000000..a31cbc89 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir2/dir2.md @@ -0,0 +1 @@ +File without a valid license diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir2/dir2.txt b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir2/dir2.txt new file mode 100644 index 00000000..a31cbc89 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir2/dir2.txt @@ -0,0 +1 @@ +File without a valid license diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir3/dir3.log b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir3/dir3.log new file mode 100644 index 00000000..a31cbc89 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir3/dir3.log @@ -0,0 +1 @@ +File without a valid license diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir3/file3.log b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir3/file3.log new file mode 100644 index 00000000..a31cbc89 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/dir3/file3.log @@ -0,0 +1 @@ +File without a valid license diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/invoker.properties b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/invoker.properties new file mode 100644 index 00000000..6e8c3479 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/invoker.properties @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +invoker.goals = clean apache-rat:check diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/pom.xml b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/pom.xml new file mode 100644 index 00000000..0ec80739 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/pom.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.apache.rat.test</groupId> + <artifactId>RAT335</artifactId> + <version>1.0</version> + + <build> + <plugins> + <plugin> + <groupId>org.apache.rat</groupId> + <artifactId>apache-rat-plugin</artifactId> + <version>@pom.version@</version> + <configuration> + + <!-- Minimize the number of active rules to keep the test minimal --> + <excludes> + <exclude>**/.gitignore</exclude> + </excludes> + <useDefaultExcludes>false</useDefaultExcludes> + <useMavenDefaultExcludes>false</useMavenDefaultExcludes> + <useEclipseDefaultExcludes>false</useEclipseDefaultExcludes> + <useIdeaDefaultExcludes>false</useIdeaDefaultExcludes> + + <parseSCMIgnoresAsExcludes>true</parseSCMIgnoresAsExcludes> + </configuration> + </plugin> + </plugins> + </build> +</project> diff --git a/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/root.md b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/root.md new file mode 100644 index 00000000..a31cbc89 --- /dev/null +++ b/apache-rat-plugin/src/test/resources/unit/RAT-335-GitIgnore/root.md @@ -0,0 +1 @@ +File without a valid license diff --git a/pom.xml b/pom.xml index c2d73b0d..ea805ee7 100644 --- a/pom.xml +++ b/pom.xml @@ -635,6 +635,10 @@ agnostic home for software distribution comprehension and audit tools. <name>Claude Warren</name> <email>cla...@apache.org</email> </contributor> + <contributor> + <name>Niels Basjes</name> + <email>nielsbas...@apache.org</email> + </contributor> </contributors> <scm> <connection>scm:git:https://gitbox.apache.org/repos/asf/creadur-rat.git</connection>