Claudenw commented on code in PR #746:
URL: https://github.com/apache/creadur-rat/pull/746#discussion_r4376994290
##########
apache-rat-core/src/main/java/org/apache/rat/api/EnvVar.java:
##########
@@ -45,6 +64,24 @@ public enum EnvVar {
* Defines the user HOME.
*/
HOME("The current user's home directory."),
+ /**
+ * The Windows user profile directory.
+ * <p>
+ * The environment variable that is read is {@code USERPROFILE} (no
underscore), which is the
+ * canonical name used on Windows. The enum constant is spelled {@code
USER_PROFILE} to stay
+ * consistent with the naming of the other constants.</p>
+ */
Review Comment:
I don't think we want to do this. We have tried not to introduce OS
specific variable and options. when you need to access the HOME directory you
can use the Java system property `user.home`
@ottlinger I think this should be removed. Do you agree?
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
Review Comment:
I don't like the `GitConfig` class in the same package as the file
processors. It is small and I would like to see it in the GitIgnoreBuilder
class.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
Review Comment:
The HOME_PREFIX should be build using the FSInfo object.
```
FSInfo fsInfo = DocumentName.FSInfo.getDefault()'
String home_prefix = "~" + fsInfo.dirSeparator()
```
you can also use `fsInfo.normalize(text)` to convert the `text` into a path
for the specified file system.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
+ /** The Windows style home directory prefix of a path. */
+ private static final String HOME_BACKSLASH_PREFIX = "~\\";
Review Comment:
not necessary with FSInfo usage.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
+ /** The Windows style home directory prefix of a path. */
+ private static final String HOME_BACKSLASH_PREFIX = "~\\";
+ /** The comment marker for a full line comment. */
+ private static final String COMMENT_PREFIX = "#";
+ /** The alternate comment marker for a full line comment. */
+ private static final String SEMICOLON_COMMENT_PREFIX = ";";
+ /** The opening quote of a quoted value. */
+ private static final String QUOTE = "\"";
+ /** The escape character of a quoted value. */
+ private static final String ESCAPE = "\\";
+
+ /** The XDG configuration file. */
+ private final File xdgConfig;
+ /** The user configuration file. */
+ private final File userConfig;
+ /** The home directory or {@code null} when not defined. */
+ private final String home;
+ /** The directory against which relative paths are resolved. */
+ private final File cwd;
+ /** The default global ignore file, next to the XDG configuration or
{@code null}. */
+ private final File defaultIgnore;
+ /** The file configured by the {@code GIT_CONFIG_GLOBAL} environment
variable or {@code null}. */
+ private final File globalConfig;
+
+ /**
+ * Creates a GitConfig reading the configuration files from the
environment.
+ */
+ public GitConfig() {
+ this(gitConfigGlobalFile(), xdgConfigFile(), userConfigFile(),
homeDir(),
+ new File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home) {
+ this(null, xdgConfig, userConfig, home, new
File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home,
final File cwd) {
+ this(null, xdgConfig, userConfig, home, cwd);
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param globalConfig the effective global config file (from {@code
GIT_CONFIG_GLOBAL})
+ * or {@code null} to use the XDG and user configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File globalConfig, final File xdgConfig, final File
userConfig, final String home, final File cwd) {
+ this.globalConfig = globalConfig;
+ this.xdgConfig = xdgConfig;
+ this.userConfig = userConfig;
+ this.home = home;
+ this.cwd = cwd;
+ File parent = xdgConfig == null ? null :
xdgConfig.getAbsoluteFile().getParentFile();
+ this.defaultIgnore = parent == null ? null : new File(parent,
"ignore");
+ }
+
+ /**
+ * Resolves the global git ignore file.
+ * <p>
+ * If {@code core.excludesFile} is configured in one of the git
configuration files the configured
+ * file is returned - even if it does not exist, matching git behavior
where a configured file
+ * disables the default location. Otherwise the default location is
returned if it exists.
+ * </p>
+ * @return the configured or default global git ignore file or an empty
Optional if neither exists.
+ */
+ public Optional<File> coreExcludesFile() {
+ List<String> configured = new ArrayList<>();
+ Set<File> includeChain = new HashSet<>();
+ if (globalConfig != null) {
+ // when GIT_CONFIG_GLOBAL is set, neither ~/.gitconfig nor
$XDG_CONFIG_HOME/git/config are read
+ parseFile(globalConfig, configured, includeChain, 0);
+ } else {
+ parseFile(xdgConfig, configured, includeChain, 0);
+ parseFile(userConfig, configured, includeChain, 0);
+ }
+ if (!configured.isEmpty()) {
+ String value = configured.get(configured.size() - 1);
+ return resolveExcludesFile(value);
+ }
+ return defaultIgnore != null && defaultIgnore.isFile() ?
Optional.of(defaultIgnore) : Optional.empty();
+ }
+
+ /**
+ * Parses a configuration file, expanding {@code [include] path}
directives in place.
+ * @param file the file to parse or {@code null} if there is none.
+ * @param excludesFileValues the collected raw {@code core.excludesFile}
values in order.
+ * @param includeChain the canonical files of the current include chain to
detect cycles.
+ * @param depth the current include depth.
+ */
+ private void parseFile(final File file, final List<String>
excludesFileValues, final Set<File> includeChain, final int depth) {
+ if (file == null || depth > MAX_INCLUDE_DEPTH) {
+ return;
+ }
+ final File canonical = canonical(file);
+ if (!canonical.isFile() || !includeChain.add(canonical)) {
+ return;
+ }
+ try (BufferedReader reader = Files.newBufferedReader(file.toPath(),
StandardCharsets.UTF_8)) {
+ String section = "";
+ String subsection = null;
+ String pending = null;
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String current = pending == null ? line : pending + line;
+ if ((trailingBackslashes(current) & 1) == 1) {
+ pending = current.substring(0, current.length() - 1);
+ continue;
+ }
+ pending = null;
+ current = current.strip();
+ if (current.isEmpty() || current.startsWith(COMMENT_PREFIX) ||
current.startsWith(SEMICOLON_COMMENT_PREFIX)) {
+ continue;
+ }
+ if (current.startsWith("[")) {
+ Section parsedSection = parseSection(current);
+ if (parsedSection != null) {
+ section = parsedSection.name();
+ subsection = parsedSection.subsection();
+ }
+ continue;
+ }
+ int separator = current.indexOf('=');
+ if (separator < 0) {
+ continue;
+ }
+ String key = current.substring(0,
separator).strip().toLowerCase(Locale.ROOT);
+ String value = current.substring(separator + 1).strip();
+ if (key.isEmpty() || value.isEmpty()) {
+ continue;
+ }
+ String unquoted = unquote(value);
+ if (CORE_SECTION.equals(section) && subsection == null &&
EXCLUDES_FILE_KEY.equals(key)) {
+ excludesFileValues.add(unquoted);
+ } else if (INCLUDE_SECTION.equals(section) && subsection ==
null && INCLUDE_PATH_KEY.equals(key)) {
+ resolveInclude(unquoted, file).ifPresent(included ->
parseFile(included, excludesFileValues, includeChain, depth + 1));
+ }
+ }
+ } catch (IOException e) {
+ // unreadable configuration files are ignored like git does
+ } finally {
+ includeChain.remove(canonical);
+ }
+ }
+
+ /**
+ * Parses a section header.
+ * @param line the line starting with '['.
+ * @return the parsed section or {@code null} if the line is not a valid
section header.
+ */
+ private Section parseSection(final String line) {
+ int end = line.indexOf(']');
+ if (end < 0) {
+ return null;
+ }
+ String inner = line.substring(1, end).strip();
+ int quote = inner.indexOf(QUOTE);
+ String name;
+ String subsection = null;
+ if (quote >= 0) {
+ name = inner.substring(0, quote).strip();
+ int close = inner.indexOf(QUOTE, quote + 1);
+ if (close >= 0) {
+ subsection = inner.substring(quote + 1, close);
+ }
+ } else {
+ int dot = inner.indexOf('.');
+ if (dot >= 0) {
+ name = inner.substring(0, dot).strip();
+ subsection = inner.substring(dot + 1).strip();
+ } else {
+ name = inner;
+ }
+ }
+ return name.isEmpty() ? null : new
Section(name.toLowerCase(Locale.ROOT), subsection);
+ }
+
+ /**
+ * Resolves an include path.
+ * @param value the raw value of the {@code include.path} key.
+ * @param includingFile the file that contains the include directive.
+ * @return the file to include or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveInclude(final String value, final File
includingFile) {
+ if (isHomePrefixed(value)) {
+ return expandHome(value);
+ }
+ if (value.startsWith("~")) {
+ return Optional.empty();
+ }
+ File path = new File(value);
+ if (!path.isAbsolute() && includingFile.getParentFile() != null) {
+ path = new File(includingFile.getParentFile(), value);
+ }
+ return Optional.of(path);
+ }
+
+ /**
+ * Resolves a {@code core.excludesFile} value.
+ * @param value the raw value of the {@code core.excludesFile} key.
+ * @return the configured file or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveExcludesFile(final String value) {
+ if (isHomePrefixed(value)) {
+ return expandHome(value);
+ }
+ if (value.startsWith("~")) {
+ return Optional.empty();
+ }
+ File path = new File(value);
+ if (!path.isAbsolute()) {
+ // git resolves relative paths against the current working
directory
+ path = new File(cwd, value);
+ }
+ return Optional.of(path);
+ }
+
+ /**
+ * Expands a {@code ~/} prefixed value against the home directory.
+ * @param value the value to expand.
+ * @return the expanded file or an empty Optional if no home directory is
defined.
+ */
+ private boolean isHomePrefixed(final String value) {
+ return value.startsWith(HOME_PREFIX) ||
value.startsWith(HOME_BACKSLASH_PREFIX);
Review Comment:
as per earlier note. this HOME_PREFIX shold be specified for the file system
type.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
+ /** The Windows style home directory prefix of a path. */
+ private static final String HOME_BACKSLASH_PREFIX = "~\\";
+ /** The comment marker for a full line comment. */
+ private static final String COMMENT_PREFIX = "#";
+ /** The alternate comment marker for a full line comment. */
+ private static final String SEMICOLON_COMMENT_PREFIX = ";";
+ /** The opening quote of a quoted value. */
+ private static final String QUOTE = "\"";
+ /** The escape character of a quoted value. */
+ private static final String ESCAPE = "\\";
+
+ /** The XDG configuration file. */
+ private final File xdgConfig;
+ /** The user configuration file. */
+ private final File userConfig;
+ /** The home directory or {@code null} when not defined. */
+ private final String home;
+ /** The directory against which relative paths are resolved. */
+ private final File cwd;
+ /** The default global ignore file, next to the XDG configuration or
{@code null}. */
+ private final File defaultIgnore;
+ /** The file configured by the {@code GIT_CONFIG_GLOBAL} environment
variable or {@code null}. */
+ private final File globalConfig;
+
+ /**
+ * Creates a GitConfig reading the configuration files from the
environment.
+ */
+ public GitConfig() {
+ this(gitConfigGlobalFile(), xdgConfigFile(), userConfigFile(),
homeDir(),
+ new File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home) {
+ this(null, xdgConfig, userConfig, home, new
File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home,
final File cwd) {
+ this(null, xdgConfig, userConfig, home, cwd);
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param globalConfig the effective global config file (from {@code
GIT_CONFIG_GLOBAL})
+ * or {@code null} to use the XDG and user configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File globalConfig, final File xdgConfig, final File
userConfig, final String home, final File cwd) {
+ this.globalConfig = globalConfig;
+ this.xdgConfig = xdgConfig;
+ this.userConfig = userConfig;
+ this.home = home;
+ this.cwd = cwd;
+ File parent = xdgConfig == null ? null :
xdgConfig.getAbsoluteFile().getParentFile();
+ this.defaultIgnore = parent == null ? null : new File(parent,
"ignore");
+ }
+
+ /**
+ * Resolves the global git ignore file.
+ * <p>
+ * If {@code core.excludesFile} is configured in one of the git
configuration files the configured
+ * file is returned - even if it does not exist, matching git behavior
where a configured file
+ * disables the default location. Otherwise the default location is
returned if it exists.
+ * </p>
+ * @return the configured or default global git ignore file or an empty
Optional if neither exists.
+ */
+ public Optional<File> coreExcludesFile() {
+ List<String> configured = new ArrayList<>();
+ Set<File> includeChain = new HashSet<>();
+ if (globalConfig != null) {
+ // when GIT_CONFIG_GLOBAL is set, neither ~/.gitconfig nor
$XDG_CONFIG_HOME/git/config are read
+ parseFile(globalConfig, configured, includeChain, 0);
+ } else {
+ parseFile(xdgConfig, configured, includeChain, 0);
+ parseFile(userConfig, configured, includeChain, 0);
+ }
+ if (!configured.isEmpty()) {
+ String value = configured.get(configured.size() - 1);
+ return resolveExcludesFile(value);
+ }
Review Comment:
This seem incorrect.
https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration
specifies that all the configuration files should be taken together and the
specification closest to the project should take prescidence.
This code does not consider the system config, nor does it consider the
.git/config file for the project.
I can not find documentation that says the use of GIT_CONFIG_GLOBAL means
that the ~/.gitconfig and XDG based config are not read.
I can see that XDG based config is designed to override the ~/.gitconfig so
only one of them would be expected.
I did find a reference that said that GIT_CONFIG_GLOBAL may contain multiple
entries. Infact setting `XDG_CONFIG_HOME` and placing a `.gitignore` in that
directory and then running `git var -l` lists both the
`${XDG_CONFIG_HOME}/.gitignore` and the `~/.gitignore` as entries for the
`GIT_CONFIG_GLOBAL` value
Explicitly setting `GIT_CONFIG_GLOBAL` does limit the `git var -l` results
to the single value.
Explicitly setting:
```
set GIT_CONFIG_GLOBAL="/path/to/dir1
/path/to/dir2"
```
will cause `git var -l` to list both files.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
+ /** The Windows style home directory prefix of a path. */
+ private static final String HOME_BACKSLASH_PREFIX = "~\\";
+ /** The comment marker for a full line comment. */
+ private static final String COMMENT_PREFIX = "#";
+ /** The alternate comment marker for a full line comment. */
+ private static final String SEMICOLON_COMMENT_PREFIX = ";";
+ /** The opening quote of a quoted value. */
+ private static final String QUOTE = "\"";
+ /** The escape character of a quoted value. */
+ private static final String ESCAPE = "\\";
+
+ /** The XDG configuration file. */
+ private final File xdgConfig;
+ /** The user configuration file. */
+ private final File userConfig;
+ /** The home directory or {@code null} when not defined. */
+ private final String home;
+ /** The directory against which relative paths are resolved. */
+ private final File cwd;
+ /** The default global ignore file, next to the XDG configuration or
{@code null}. */
+ private final File defaultIgnore;
+ /** The file configured by the {@code GIT_CONFIG_GLOBAL} environment
variable or {@code null}. */
+ private final File globalConfig;
+
+ /**
+ * Creates a GitConfig reading the configuration files from the
environment.
+ */
+ public GitConfig() {
+ this(gitConfigGlobalFile(), xdgConfigFile(), userConfigFile(),
homeDir(),
+ new File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home) {
+ this(null, xdgConfig, userConfig, home, new
File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home,
final File cwd) {
+ this(null, xdgConfig, userConfig, home, cwd);
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param globalConfig the effective global config file (from {@code
GIT_CONFIG_GLOBAL})
+ * or {@code null} to use the XDG and user configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File globalConfig, final File xdgConfig, final File
userConfig, final String home, final File cwd) {
+ this.globalConfig = globalConfig;
+ this.xdgConfig = xdgConfig;
+ this.userConfig = userConfig;
+ this.home = home;
+ this.cwd = cwd;
+ File parent = xdgConfig == null ? null :
xdgConfig.getAbsoluteFile().getParentFile();
+ this.defaultIgnore = parent == null ? null : new File(parent,
"ignore");
+ }
+
+ /**
+ * Resolves the global git ignore file.
+ * <p>
+ * If {@code core.excludesFile} is configured in one of the git
configuration files the configured
+ * file is returned - even if it does not exist, matching git behavior
where a configured file
+ * disables the default location. Otherwise the default location is
returned if it exists.
+ * </p>
+ * @return the configured or default global git ignore file or an empty
Optional if neither exists.
+ */
+ public Optional<File> coreExcludesFile() {
+ List<String> configured = new ArrayList<>();
+ Set<File> includeChain = new HashSet<>();
+ if (globalConfig != null) {
+ // when GIT_CONFIG_GLOBAL is set, neither ~/.gitconfig nor
$XDG_CONFIG_HOME/git/config are read
+ parseFile(globalConfig, configured, includeChain, 0);
+ } else {
+ parseFile(xdgConfig, configured, includeChain, 0);
+ parseFile(userConfig, configured, includeChain, 0);
+ }
+ if (!configured.isEmpty()) {
+ String value = configured.get(configured.size() - 1);
+ return resolveExcludesFile(value);
+ }
+ return defaultIgnore != null && defaultIgnore.isFile() ?
Optional.of(defaultIgnore) : Optional.empty();
+ }
+
+ /**
+ * Parses a configuration file, expanding {@code [include] path}
directives in place.
+ * @param file the file to parse or {@code null} if there is none.
+ * @param excludesFileValues the collected raw {@code core.excludesFile}
values in order.
+ * @param includeChain the canonical files of the current include chain to
detect cycles.
+ * @param depth the current include depth.
+ */
+ private void parseFile(final File file, final List<String>
excludesFileValues, final Set<File> includeChain, final int depth) {
Review Comment:
This is parsing what is effectively an INI file. Please use Apache commons
configuration2 INICoanfiguration.
Since the documentation for the configurations specifies the order in which
the options are accepted (which overrides which) you can use the
`CombinedConfiguration` to get the proper layering to return the values.
In addition, you can parse the input into multiple lines by using
`BufferedReader.readLine()` constructed on the result from fetching
configuration the value.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
+ /** The Windows style home directory prefix of a path. */
+ private static final String HOME_BACKSLASH_PREFIX = "~\\";
+ /** The comment marker for a full line comment. */
+ private static final String COMMENT_PREFIX = "#";
+ /** The alternate comment marker for a full line comment. */
+ private static final String SEMICOLON_COMMENT_PREFIX = ";";
+ /** The opening quote of a quoted value. */
+ private static final String QUOTE = "\"";
+ /** The escape character of a quoted value. */
+ private static final String ESCAPE = "\\";
+
+ /** The XDG configuration file. */
+ private final File xdgConfig;
+ /** The user configuration file. */
+ private final File userConfig;
+ /** The home directory or {@code null} when not defined. */
+ private final String home;
+ /** The directory against which relative paths are resolved. */
+ private final File cwd;
+ /** The default global ignore file, next to the XDG configuration or
{@code null}. */
+ private final File defaultIgnore;
+ /** The file configured by the {@code GIT_CONFIG_GLOBAL} environment
variable or {@code null}. */
+ private final File globalConfig;
+
+ /**
+ * Creates a GitConfig reading the configuration files from the
environment.
+ */
+ public GitConfig() {
+ this(gitConfigGlobalFile(), xdgConfigFile(), userConfigFile(),
homeDir(),
+ new File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home) {
+ this(null, xdgConfig, userConfig, home, new
File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home,
final File cwd) {
+ this(null, xdgConfig, userConfig, home, cwd);
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param globalConfig the effective global config file (from {@code
GIT_CONFIG_GLOBAL})
+ * or {@code null} to use the XDG and user configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File globalConfig, final File xdgConfig, final File
userConfig, final String home, final File cwd) {
+ this.globalConfig = globalConfig;
+ this.xdgConfig = xdgConfig;
+ this.userConfig = userConfig;
+ this.home = home;
+ this.cwd = cwd;
+ File parent = xdgConfig == null ? null :
xdgConfig.getAbsoluteFile().getParentFile();
+ this.defaultIgnore = parent == null ? null : new File(parent,
"ignore");
+ }
+
+ /**
+ * Resolves the global git ignore file.
+ * <p>
+ * If {@code core.excludesFile} is configured in one of the git
configuration files the configured
+ * file is returned - even if it does not exist, matching git behavior
where a configured file
+ * disables the default location. Otherwise the default location is
returned if it exists.
+ * </p>
+ * @return the configured or default global git ignore file or an empty
Optional if neither exists.
+ */
+ public Optional<File> coreExcludesFile() {
+ List<String> configured = new ArrayList<>();
+ Set<File> includeChain = new HashSet<>();
+ if (globalConfig != null) {
+ // when GIT_CONFIG_GLOBAL is set, neither ~/.gitconfig nor
$XDG_CONFIG_HOME/git/config are read
+ parseFile(globalConfig, configured, includeChain, 0);
+ } else {
+ parseFile(xdgConfig, configured, includeChain, 0);
+ parseFile(userConfig, configured, includeChain, 0);
+ }
+ if (!configured.isEmpty()) {
+ String value = configured.get(configured.size() - 1);
+ return resolveExcludesFile(value);
+ }
+ return defaultIgnore != null && defaultIgnore.isFile() ?
Optional.of(defaultIgnore) : Optional.empty();
+ }
+
+ /**
+ * Parses a configuration file, expanding {@code [include] path}
directives in place.
+ * @param file the file to parse or {@code null} if there is none.
+ * @param excludesFileValues the collected raw {@code core.excludesFile}
values in order.
+ * @param includeChain the canonical files of the current include chain to
detect cycles.
+ * @param depth the current include depth.
+ */
+ private void parseFile(final File file, final List<String>
excludesFileValues, final Set<File> includeChain, final int depth) {
+ if (file == null || depth > MAX_INCLUDE_DEPTH) {
+ return;
+ }
+ final File canonical = canonical(file);
+ if (!canonical.isFile() || !includeChain.add(canonical)) {
+ return;
+ }
+ try (BufferedReader reader = Files.newBufferedReader(file.toPath(),
StandardCharsets.UTF_8)) {
+ String section = "";
+ String subsection = null;
+ String pending = null;
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String current = pending == null ? line : pending + line;
+ if ((trailingBackslashes(current) & 1) == 1) {
+ pending = current.substring(0, current.length() - 1);
+ continue;
+ }
+ pending = null;
+ current = current.strip();
+ if (current.isEmpty() || current.startsWith(COMMENT_PREFIX) ||
current.startsWith(SEMICOLON_COMMENT_PREFIX)) {
+ continue;
+ }
+ if (current.startsWith("[")) {
+ Section parsedSection = parseSection(current);
+ if (parsedSection != null) {
+ section = parsedSection.name();
+ subsection = parsedSection.subsection();
+ }
+ continue;
+ }
+ int separator = current.indexOf('=');
+ if (separator < 0) {
+ continue;
+ }
+ String key = current.substring(0,
separator).strip().toLowerCase(Locale.ROOT);
+ String value = current.substring(separator + 1).strip();
+ if (key.isEmpty() || value.isEmpty()) {
+ continue;
+ }
+ String unquoted = unquote(value);
+ if (CORE_SECTION.equals(section) && subsection == null &&
EXCLUDES_FILE_KEY.equals(key)) {
+ excludesFileValues.add(unquoted);
+ } else if (INCLUDE_SECTION.equals(section) && subsection ==
null && INCLUDE_PATH_KEY.equals(key)) {
+ resolveInclude(unquoted, file).ifPresent(included ->
parseFile(included, excludesFileValues, includeChain, depth + 1));
+ }
+ }
+ } catch (IOException e) {
+ // unreadable configuration files are ignored like git does
+ } finally {
+ includeChain.remove(canonical);
+ }
+ }
+
+ /**
+ * Parses a section header.
+ * @param line the line starting with '['.
+ * @return the parsed section or {@code null} if the line is not a valid
section header.
+ */
+ private Section parseSection(final String line) {
+ int end = line.indexOf(']');
+ if (end < 0) {
+ return null;
+ }
+ String inner = line.substring(1, end).strip();
+ int quote = inner.indexOf(QUOTE);
+ String name;
+ String subsection = null;
+ if (quote >= 0) {
+ name = inner.substring(0, quote).strip();
+ int close = inner.indexOf(QUOTE, quote + 1);
+ if (close >= 0) {
+ subsection = inner.substring(quote + 1, close);
+ }
+ } else {
+ int dot = inner.indexOf('.');
+ if (dot >= 0) {
+ name = inner.substring(0, dot).strip();
+ subsection = inner.substring(dot + 1).strip();
+ } else {
+ name = inner;
+ }
+ }
+ return name.isEmpty() ? null : new
Section(name.toLowerCase(Locale.ROOT), subsection);
+ }
+
+ /**
+ * Resolves an include path.
+ * @param value the raw value of the {@code include.path} key.
+ * @param includingFile the file that contains the include directive.
+ * @return the file to include or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveInclude(final String value, final File
includingFile) {
Review Comment:
`resolveInclude` and `resolveExclude` seem to be the same. Perhaps a single
method to do the resolution is warranted.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
+ /** The Windows style home directory prefix of a path. */
+ private static final String HOME_BACKSLASH_PREFIX = "~\\";
+ /** The comment marker for a full line comment. */
+ private static final String COMMENT_PREFIX = "#";
+ /** The alternate comment marker for a full line comment. */
+ private static final String SEMICOLON_COMMENT_PREFIX = ";";
+ /** The opening quote of a quoted value. */
+ private static final String QUOTE = "\"";
+ /** The escape character of a quoted value. */
+ private static final String ESCAPE = "\\";
+
+ /** The XDG configuration file. */
+ private final File xdgConfig;
+ /** The user configuration file. */
+ private final File userConfig;
+ /** The home directory or {@code null} when not defined. */
+ private final String home;
+ /** The directory against which relative paths are resolved. */
+ private final File cwd;
+ /** The default global ignore file, next to the XDG configuration or
{@code null}. */
+ private final File defaultIgnore;
+ /** The file configured by the {@code GIT_CONFIG_GLOBAL} environment
variable or {@code null}. */
+ private final File globalConfig;
+
+ /**
+ * Creates a GitConfig reading the configuration files from the
environment.
+ */
+ public GitConfig() {
+ this(gitConfigGlobalFile(), xdgConfigFile(), userConfigFile(),
homeDir(),
+ new File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home) {
+ this(null, xdgConfig, userConfig, home, new
File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home,
final File cwd) {
+ this(null, xdgConfig, userConfig, home, cwd);
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param globalConfig the effective global config file (from {@code
GIT_CONFIG_GLOBAL})
+ * or {@code null} to use the XDG and user configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File globalConfig, final File xdgConfig, final File
userConfig, final String home, final File cwd) {
+ this.globalConfig = globalConfig;
+ this.xdgConfig = xdgConfig;
+ this.userConfig = userConfig;
+ this.home = home;
+ this.cwd = cwd;
+ File parent = xdgConfig == null ? null :
xdgConfig.getAbsoluteFile().getParentFile();
+ this.defaultIgnore = parent == null ? null : new File(parent,
"ignore");
+ }
+
+ /**
+ * Resolves the global git ignore file.
+ * <p>
+ * If {@code core.excludesFile} is configured in one of the git
configuration files the configured
+ * file is returned - even if it does not exist, matching git behavior
where a configured file
+ * disables the default location. Otherwise the default location is
returned if it exists.
+ * </p>
+ * @return the configured or default global git ignore file or an empty
Optional if neither exists.
+ */
+ public Optional<File> coreExcludesFile() {
+ List<String> configured = new ArrayList<>();
+ Set<File> includeChain = new HashSet<>();
+ if (globalConfig != null) {
+ // when GIT_CONFIG_GLOBAL is set, neither ~/.gitconfig nor
$XDG_CONFIG_HOME/git/config are read
+ parseFile(globalConfig, configured, includeChain, 0);
+ } else {
+ parseFile(xdgConfig, configured, includeChain, 0);
+ parseFile(userConfig, configured, includeChain, 0);
+ }
+ if (!configured.isEmpty()) {
+ String value = configured.get(configured.size() - 1);
+ return resolveExcludesFile(value);
+ }
+ return defaultIgnore != null && defaultIgnore.isFile() ?
Optional.of(defaultIgnore) : Optional.empty();
+ }
+
+ /**
+ * Parses a configuration file, expanding {@code [include] path}
directives in place.
+ * @param file the file to parse or {@code null} if there is none.
+ * @param excludesFileValues the collected raw {@code core.excludesFile}
values in order.
+ * @param includeChain the canonical files of the current include chain to
detect cycles.
+ * @param depth the current include depth.
+ */
+ private void parseFile(final File file, final List<String>
excludesFileValues, final Set<File> includeChain, final int depth) {
+ if (file == null || depth > MAX_INCLUDE_DEPTH) {
+ return;
+ }
+ final File canonical = canonical(file);
+ if (!canonical.isFile() || !includeChain.add(canonical)) {
+ return;
+ }
+ try (BufferedReader reader = Files.newBufferedReader(file.toPath(),
StandardCharsets.UTF_8)) {
+ String section = "";
+ String subsection = null;
+ String pending = null;
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String current = pending == null ? line : pending + line;
+ if ((trailingBackslashes(current) & 1) == 1) {
+ pending = current.substring(0, current.length() - 1);
+ continue;
+ }
+ pending = null;
+ current = current.strip();
+ if (current.isEmpty() || current.startsWith(COMMENT_PREFIX) ||
current.startsWith(SEMICOLON_COMMENT_PREFIX)) {
+ continue;
+ }
+ if (current.startsWith("[")) {
+ Section parsedSection = parseSection(current);
+ if (parsedSection != null) {
+ section = parsedSection.name();
+ subsection = parsedSection.subsection();
+ }
+ continue;
+ }
+ int separator = current.indexOf('=');
+ if (separator < 0) {
+ continue;
+ }
+ String key = current.substring(0,
separator).strip().toLowerCase(Locale.ROOT);
+ String value = current.substring(separator + 1).strip();
+ if (key.isEmpty() || value.isEmpty()) {
+ continue;
+ }
+ String unquoted = unquote(value);
+ if (CORE_SECTION.equals(section) && subsection == null &&
EXCLUDES_FILE_KEY.equals(key)) {
+ excludesFileValues.add(unquoted);
+ } else if (INCLUDE_SECTION.equals(section) && subsection ==
null && INCLUDE_PATH_KEY.equals(key)) {
+ resolveInclude(unquoted, file).ifPresent(included ->
parseFile(included, excludesFileValues, includeChain, depth + 1));
+ }
+ }
+ } catch (IOException e) {
+ // unreadable configuration files are ignored like git does
+ } finally {
+ includeChain.remove(canonical);
+ }
+ }
+
+ /**
+ * Parses a section header.
+ * @param line the line starting with '['.
+ * @return the parsed section or {@code null} if the line is not a valid
section header.
+ */
+ private Section parseSection(final String line) {
+ int end = line.indexOf(']');
+ if (end < 0) {
+ return null;
+ }
+ String inner = line.substring(1, end).strip();
+ int quote = inner.indexOf(QUOTE);
+ String name;
+ String subsection = null;
+ if (quote >= 0) {
+ name = inner.substring(0, quote).strip();
+ int close = inner.indexOf(QUOTE, quote + 1);
+ if (close >= 0) {
+ subsection = inner.substring(quote + 1, close);
+ }
+ } else {
+ int dot = inner.indexOf('.');
+ if (dot >= 0) {
+ name = inner.substring(0, dot).strip();
+ subsection = inner.substring(dot + 1).strip();
+ } else {
+ name = inner;
+ }
+ }
+ return name.isEmpty() ? null : new
Section(name.toLowerCase(Locale.ROOT), subsection);
+ }
+
+ /**
+ * Resolves an include path.
+ * @param value the raw value of the {@code include.path} key.
+ * @param includingFile the file that contains the include directive.
+ * @return the file to include or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveInclude(final String value, final File
includingFile) {
+ if (isHomePrefixed(value)) {
+ return expandHome(value);
+ }
+ if (value.startsWith("~")) {
+ return Optional.empty();
+ }
+ File path = new File(value);
+ if (!path.isAbsolute() && includingFile.getParentFile() != null) {
+ path = new File(includingFile.getParentFile(), value);
+ }
+ return Optional.of(path);
+ }
+
+ /**
+ * Resolves a {@code core.excludesFile} value.
+ * @param value the raw value of the {@code core.excludesFile} key.
+ * @return the configured file or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveExcludesFile(final String value) {
+ if (isHomePrefixed(value)) {
+ return expandHome(value);
+ }
+ if (value.startsWith("~")) {
+ return Optional.empty();
+ }
+ File path = new File(value);
+ if (!path.isAbsolute()) {
+ // git resolves relative paths against the current working
directory
+ path = new File(cwd, value);
Review Comment:
I did some extensive testing and I could not find a configuration where
relative paths were resolved within the current working directory. Git always
seems to resolve the file relative to the INI file that it was read from.
you can use `find . | git check-ignore -v -n --stdin --no-index` to evaluate
a configuration. I execute this in the
`resources/GitIgnoreBuilderTest/RAT-578-root` directory with multiple
configurations.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
+ /** The Windows style home directory prefix of a path. */
+ private static final String HOME_BACKSLASH_PREFIX = "~\\";
+ /** The comment marker for a full line comment. */
+ private static final String COMMENT_PREFIX = "#";
+ /** The alternate comment marker for a full line comment. */
+ private static final String SEMICOLON_COMMENT_PREFIX = ";";
+ /** The opening quote of a quoted value. */
+ private static final String QUOTE = "\"";
+ /** The escape character of a quoted value. */
+ private static final String ESCAPE = "\\";
+
+ /** The XDG configuration file. */
+ private final File xdgConfig;
+ /** The user configuration file. */
+ private final File userConfig;
+ /** The home directory or {@code null} when not defined. */
+ private final String home;
+ /** The directory against which relative paths are resolved. */
+ private final File cwd;
+ /** The default global ignore file, next to the XDG configuration or
{@code null}. */
+ private final File defaultIgnore;
+ /** The file configured by the {@code GIT_CONFIG_GLOBAL} environment
variable or {@code null}. */
+ private final File globalConfig;
+
+ /**
+ * Creates a GitConfig reading the configuration files from the
environment.
+ */
+ public GitConfig() {
+ this(gitConfigGlobalFile(), xdgConfigFile(), userConfigFile(),
homeDir(),
+ new File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home) {
+ this(null, xdgConfig, userConfig, home, new
File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home,
final File cwd) {
+ this(null, xdgConfig, userConfig, home, cwd);
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param globalConfig the effective global config file (from {@code
GIT_CONFIG_GLOBAL})
+ * or {@code null} to use the XDG and user configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File globalConfig, final File xdgConfig, final File
userConfig, final String home, final File cwd) {
+ this.globalConfig = globalConfig;
+ this.xdgConfig = xdgConfig;
+ this.userConfig = userConfig;
+ this.home = home;
+ this.cwd = cwd;
+ File parent = xdgConfig == null ? null :
xdgConfig.getAbsoluteFile().getParentFile();
+ this.defaultIgnore = parent == null ? null : new File(parent,
"ignore");
+ }
+
+ /**
+ * Resolves the global git ignore file.
+ * <p>
+ * If {@code core.excludesFile} is configured in one of the git
configuration files the configured
+ * file is returned - even if it does not exist, matching git behavior
where a configured file
+ * disables the default location. Otherwise the default location is
returned if it exists.
+ * </p>
+ * @return the configured or default global git ignore file or an empty
Optional if neither exists.
+ */
+ public Optional<File> coreExcludesFile() {
+ List<String> configured = new ArrayList<>();
+ Set<File> includeChain = new HashSet<>();
+ if (globalConfig != null) {
+ // when GIT_CONFIG_GLOBAL is set, neither ~/.gitconfig nor
$XDG_CONFIG_HOME/git/config are read
+ parseFile(globalConfig, configured, includeChain, 0);
+ } else {
+ parseFile(xdgConfig, configured, includeChain, 0);
+ parseFile(userConfig, configured, includeChain, 0);
+ }
+ if (!configured.isEmpty()) {
+ String value = configured.get(configured.size() - 1);
+ return resolveExcludesFile(value);
+ }
+ return defaultIgnore != null && defaultIgnore.isFile() ?
Optional.of(defaultIgnore) : Optional.empty();
+ }
+
+ /**
+ * Parses a configuration file, expanding {@code [include] path}
directives in place.
+ * @param file the file to parse or {@code null} if there is none.
+ * @param excludesFileValues the collected raw {@code core.excludesFile}
values in order.
+ * @param includeChain the canonical files of the current include chain to
detect cycles.
+ * @param depth the current include depth.
+ */
+ private void parseFile(final File file, final List<String>
excludesFileValues, final Set<File> includeChain, final int depth) {
+ if (file == null || depth > MAX_INCLUDE_DEPTH) {
+ return;
+ }
+ final File canonical = canonical(file);
+ if (!canonical.isFile() || !includeChain.add(canonical)) {
+ return;
+ }
+ try (BufferedReader reader = Files.newBufferedReader(file.toPath(),
StandardCharsets.UTF_8)) {
+ String section = "";
+ String subsection = null;
+ String pending = null;
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String current = pending == null ? line : pending + line;
+ if ((trailingBackslashes(current) & 1) == 1) {
+ pending = current.substring(0, current.length() - 1);
+ continue;
+ }
+ pending = null;
+ current = current.strip();
+ if (current.isEmpty() || current.startsWith(COMMENT_PREFIX) ||
current.startsWith(SEMICOLON_COMMENT_PREFIX)) {
+ continue;
+ }
+ if (current.startsWith("[")) {
+ Section parsedSection = parseSection(current);
+ if (parsedSection != null) {
+ section = parsedSection.name();
+ subsection = parsedSection.subsection();
+ }
+ continue;
+ }
+ int separator = current.indexOf('=');
+ if (separator < 0) {
+ continue;
+ }
+ String key = current.substring(0,
separator).strip().toLowerCase(Locale.ROOT);
+ String value = current.substring(separator + 1).strip();
+ if (key.isEmpty() || value.isEmpty()) {
+ continue;
+ }
+ String unquoted = unquote(value);
+ if (CORE_SECTION.equals(section) && subsection == null &&
EXCLUDES_FILE_KEY.equals(key)) {
+ excludesFileValues.add(unquoted);
+ } else if (INCLUDE_SECTION.equals(section) && subsection ==
null && INCLUDE_PATH_KEY.equals(key)) {
+ resolveInclude(unquoted, file).ifPresent(included ->
parseFile(included, excludesFileValues, includeChain, depth + 1));
+ }
+ }
+ } catch (IOException e) {
+ // unreadable configuration files are ignored like git does
+ } finally {
+ includeChain.remove(canonical);
+ }
+ }
+
+ /**
+ * Parses a section header.
+ * @param line the line starting with '['.
+ * @return the parsed section or {@code null} if the line is not a valid
section header.
+ */
+ private Section parseSection(final String line) {
+ int end = line.indexOf(']');
+ if (end < 0) {
+ return null;
+ }
+ String inner = line.substring(1, end).strip();
+ int quote = inner.indexOf(QUOTE);
+ String name;
+ String subsection = null;
+ if (quote >= 0) {
+ name = inner.substring(0, quote).strip();
+ int close = inner.indexOf(QUOTE, quote + 1);
+ if (close >= 0) {
+ subsection = inner.substring(quote + 1, close);
+ }
+ } else {
+ int dot = inner.indexOf('.');
+ if (dot >= 0) {
+ name = inner.substring(0, dot).strip();
+ subsection = inner.substring(dot + 1).strip();
+ } else {
+ name = inner;
+ }
+ }
+ return name.isEmpty() ? null : new
Section(name.toLowerCase(Locale.ROOT), subsection);
+ }
+
+ /**
+ * Resolves an include path.
+ * @param value the raw value of the {@code include.path} key.
+ * @param includingFile the file that contains the include directive.
+ * @return the file to include or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveInclude(final String value, final File
includingFile) {
+ if (isHomePrefixed(value)) {
+ return expandHome(value);
+ }
+ if (value.startsWith("~")) {
+ return Optional.empty();
+ }
+ File path = new File(value);
+ if (!path.isAbsolute() && includingFile.getParentFile() != null) {
+ path = new File(includingFile.getParentFile(), value);
+ }
+ return Optional.of(path);
+ }
+
+ /**
+ * Resolves a {@code core.excludesFile} value.
+ * @param value the raw value of the {@code core.excludesFile} key.
+ * @return the configured file or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveExcludesFile(final String value) {
+ if (isHomePrefixed(value)) {
+ return expandHome(value);
+ }
+ if (value.startsWith("~")) {
+ return Optional.empty();
+ }
+ File path = new File(value);
+ if (!path.isAbsolute()) {
+ // git resolves relative paths against the current working
directory
+ path = new File(cwd, value);
+ }
+ return Optional.of(path);
+ }
+
+ /**
+ * Expands a {@code ~/} prefixed value against the home directory.
+ * @param value the value to expand.
+ * @return the expanded file or an empty Optional if no home directory is
defined.
+ */
+ private boolean isHomePrefixed(final String value) {
+ return value.startsWith(HOME_PREFIX) ||
value.startsWith(HOME_BACKSLASH_PREFIX);
+ }
+
+ private Optional<File> expandHome(final String value) {
+ if (home == null || home.isEmpty()) {
+ return Optional.empty();
+ }
+ String rest = value.startsWith(HOME_PREFIX) ?
value.substring(HOME_PREFIX.length())
+ : value.substring(HOME_BACKSLASH_PREFIX.length());
+ return Optional.of(new File(home, rest));
+ }
+
+ /**
+ * Counts the number of trailing backslashes of a line.
+ * @param line the line to check.
+ * @return the number of trailing backslashes.
+ */
+ private int trailingBackslashes(final String line) {
+ int count = 0;
+ for (int i = line.length() - 1; i >= 0 && line.charAt(i) == '\\'; i--)
{
+ count++;
+ }
+ return count;
+ }
+
+ /**
+ * Unquotes a value if it is quoted, resolving the git escape sequences.
+ * @param value the raw value.
+ * @return the unquoted value.
+ */
+ private String unquote(final String value) {
Review Comment:
I believe that this also goes away with use of INIConfiguraiton.
##########
apache-rat-core/src/main/java/org/apache/rat/config/exclusion/fileprocessors/GitConfig.java:
##########
@@ -0,0 +1,462 @@
+/*
+ * 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.
+ */
+package org.apache.rat.config.exclusion.fileprocessors;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.rat.api.EnvVar;
+
+/**
+ * Reads the global git configuration files to resolve the file configured
+ * for {@code core.excludesFile}.
+ * <p>
+ * The configuration files are read in git order: {@code
$XDG_CONFIG_HOME/git/config}
+ * first and {@code ~/.gitconfig} second. The last value found for
+ * {@code core.excludesFile} wins.
+ * </p>
+ * <p>
+ * {@code [include] path} directives are supported and the included files are
expanded
+ * in place, recursively. The relative paths of included files are resolved
against the
+ * directory of the including file, {@code ~/} is expanded to the home
directory and a
+ * depth limit of {@value #MAX_INCLUDE_DEPTH} is enforced like git does.
+ * </p>
+ * <p>
+ * {@code includeIf} directives, the system configuration ({@code
/etc/gitconfig}), the
+ * {@code GIT_CONFIG_SYSTEM} variable and the repository configuration ({@code
.git/config}) are
+ * not supported. The {@code GIT_CONFIG_GLOBAL} variable, when set, replaces
the
+ * {@code $HOME/.gitconfig} and {@code $XDG_CONFIG_HOME/git/config} files.
+ * </p>
+ * <p>
+ * The home directory used for {@code ~} expansion is resolved from {@code
HOME}, falling back to
+ * {@link org.apache.rat.api.EnvVar#USER_PROFILE} (Windows, backed by the
{@code USERPROFILE}
+ * environment variable) and then the {@code user.home} system property.
+ * </p>
+ * <p>
+ * When {@code core.excludesFile} is not configured the default location next
to the XDG
+ * configuration ({@code $XDG_CONFIG_HOME/git/ignore} or {@code
$HOME/.config/git/ignore})
+ * is used.
+ * </p>
+ */
+public class GitConfig {
+ /** The name of the 'core' section. */
+ private static final String CORE_SECTION = "core";
+ /** The name of the 'include' section. */
+ private static final String INCLUDE_SECTION = "include";
+ /** The key of the core section holding the global ignore file. */
+ private static final String EXCLUDES_FILE_KEY = "excludesfile";
+ /** The key of the include section holding the file to include. */
+ private static final String INCLUDE_PATH_KEY = "path";
+ /** The maximum depth of nested includes, matching the git limit. */
+ private static final int MAX_INCLUDE_DEPTH = 10;
+ /** The home directory prefix of a path. */
+ private static final String HOME_PREFIX = "~/";
+ /** The Windows style home directory prefix of a path. */
+ private static final String HOME_BACKSLASH_PREFIX = "~\\";
+ /** The comment marker for a full line comment. */
+ private static final String COMMENT_PREFIX = "#";
+ /** The alternate comment marker for a full line comment. */
+ private static final String SEMICOLON_COMMENT_PREFIX = ";";
+ /** The opening quote of a quoted value. */
+ private static final String QUOTE = "\"";
+ /** The escape character of a quoted value. */
+ private static final String ESCAPE = "\\";
+
+ /** The XDG configuration file. */
+ private final File xdgConfig;
+ /** The user configuration file. */
+ private final File userConfig;
+ /** The home directory or {@code null} when not defined. */
+ private final String home;
+ /** The directory against which relative paths are resolved. */
+ private final File cwd;
+ /** The default global ignore file, next to the XDG configuration or
{@code null}. */
+ private final File defaultIgnore;
+ /** The file configured by the {@code GIT_CONFIG_GLOBAL} environment
variable or {@code null}. */
+ private final File globalConfig;
+
+ /**
+ * Creates a GitConfig reading the configuration files from the
environment.
+ */
+ public GitConfig() {
+ this(gitConfigGlobalFile(), xdgConfigFile(), userConfigFile(),
homeDir(),
+ new File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home) {
+ this(null, xdgConfig, userConfig, home, new
File(System.getProperty("user.dir")));
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~/} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File xdgConfig, final File userConfig, final String home,
final File cwd) {
+ this(null, xdgConfig, userConfig, home, cwd);
+ }
+
+ /**
+ * Creates a GitConfig reading the given configuration files.
+ * @param globalConfig the effective global config file (from {@code
GIT_CONFIG_GLOBAL})
+ * or {@code null} to use the XDG and user configuration files.
+ * @param xdgConfig the XDG configuration file or {@code null} if there is
none.
+ * @param userConfig the user configuration file or {@code null} if there
is none.
+ * @param home the home directory used for {@code ~} expansion.
+ * @param cwd the directory against which relative paths are resolved.
+ */
+ GitConfig(final File globalConfig, final File xdgConfig, final File
userConfig, final String home, final File cwd) {
+ this.globalConfig = globalConfig;
+ this.xdgConfig = xdgConfig;
+ this.userConfig = userConfig;
+ this.home = home;
+ this.cwd = cwd;
+ File parent = xdgConfig == null ? null :
xdgConfig.getAbsoluteFile().getParentFile();
+ this.defaultIgnore = parent == null ? null : new File(parent,
"ignore");
+ }
+
+ /**
+ * Resolves the global git ignore file.
+ * <p>
+ * If {@code core.excludesFile} is configured in one of the git
configuration files the configured
+ * file is returned - even if it does not exist, matching git behavior
where a configured file
+ * disables the default location. Otherwise the default location is
returned if it exists.
+ * </p>
+ * @return the configured or default global git ignore file or an empty
Optional if neither exists.
+ */
+ public Optional<File> coreExcludesFile() {
+ List<String> configured = new ArrayList<>();
+ Set<File> includeChain = new HashSet<>();
+ if (globalConfig != null) {
+ // when GIT_CONFIG_GLOBAL is set, neither ~/.gitconfig nor
$XDG_CONFIG_HOME/git/config are read
+ parseFile(globalConfig, configured, includeChain, 0);
+ } else {
+ parseFile(xdgConfig, configured, includeChain, 0);
+ parseFile(userConfig, configured, includeChain, 0);
+ }
+ if (!configured.isEmpty()) {
+ String value = configured.get(configured.size() - 1);
+ return resolveExcludesFile(value);
+ }
+ return defaultIgnore != null && defaultIgnore.isFile() ?
Optional.of(defaultIgnore) : Optional.empty();
+ }
+
+ /**
+ * Parses a configuration file, expanding {@code [include] path}
directives in place.
+ * @param file the file to parse or {@code null} if there is none.
+ * @param excludesFileValues the collected raw {@code core.excludesFile}
values in order.
+ * @param includeChain the canonical files of the current include chain to
detect cycles.
+ * @param depth the current include depth.
+ */
+ private void parseFile(final File file, final List<String>
excludesFileValues, final Set<File> includeChain, final int depth) {
+ if (file == null || depth > MAX_INCLUDE_DEPTH) {
+ return;
+ }
+ final File canonical = canonical(file);
+ if (!canonical.isFile() || !includeChain.add(canonical)) {
+ return;
+ }
+ try (BufferedReader reader = Files.newBufferedReader(file.toPath(),
StandardCharsets.UTF_8)) {
+ String section = "";
+ String subsection = null;
+ String pending = null;
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String current = pending == null ? line : pending + line;
+ if ((trailingBackslashes(current) & 1) == 1) {
+ pending = current.substring(0, current.length() - 1);
+ continue;
+ }
+ pending = null;
+ current = current.strip();
+ if (current.isEmpty() || current.startsWith(COMMENT_PREFIX) ||
current.startsWith(SEMICOLON_COMMENT_PREFIX)) {
+ continue;
+ }
+ if (current.startsWith("[")) {
+ Section parsedSection = parseSection(current);
+ if (parsedSection != null) {
+ section = parsedSection.name();
+ subsection = parsedSection.subsection();
+ }
+ continue;
+ }
+ int separator = current.indexOf('=');
+ if (separator < 0) {
+ continue;
+ }
+ String key = current.substring(0,
separator).strip().toLowerCase(Locale.ROOT);
+ String value = current.substring(separator + 1).strip();
+ if (key.isEmpty() || value.isEmpty()) {
+ continue;
+ }
+ String unquoted = unquote(value);
+ if (CORE_SECTION.equals(section) && subsection == null &&
EXCLUDES_FILE_KEY.equals(key)) {
+ excludesFileValues.add(unquoted);
+ } else if (INCLUDE_SECTION.equals(section) && subsection ==
null && INCLUDE_PATH_KEY.equals(key)) {
+ resolveInclude(unquoted, file).ifPresent(included ->
parseFile(included, excludesFileValues, includeChain, depth + 1));
+ }
+ }
+ } catch (IOException e) {
+ // unreadable configuration files are ignored like git does
+ } finally {
+ includeChain.remove(canonical);
+ }
+ }
+
+ /**
+ * Parses a section header.
+ * @param line the line starting with '['.
+ * @return the parsed section or {@code null} if the line is not a valid
section header.
+ */
+ private Section parseSection(final String line) {
+ int end = line.indexOf(']');
+ if (end < 0) {
+ return null;
+ }
+ String inner = line.substring(1, end).strip();
+ int quote = inner.indexOf(QUOTE);
+ String name;
+ String subsection = null;
+ if (quote >= 0) {
+ name = inner.substring(0, quote).strip();
+ int close = inner.indexOf(QUOTE, quote + 1);
+ if (close >= 0) {
+ subsection = inner.substring(quote + 1, close);
+ }
+ } else {
+ int dot = inner.indexOf('.');
+ if (dot >= 0) {
+ name = inner.substring(0, dot).strip();
+ subsection = inner.substring(dot + 1).strip();
+ } else {
+ name = inner;
+ }
+ }
+ return name.isEmpty() ? null : new
Section(name.toLowerCase(Locale.ROOT), subsection);
+ }
+
+ /**
+ * Resolves an include path.
+ * @param value the raw value of the {@code include.path} key.
+ * @param includingFile the file that contains the include directive.
+ * @return the file to include or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveInclude(final String value, final File
includingFile) {
+ if (isHomePrefixed(value)) {
+ return expandHome(value);
+ }
+ if (value.startsWith("~")) {
+ return Optional.empty();
+ }
+ File path = new File(value);
+ if (!path.isAbsolute() && includingFile.getParentFile() != null) {
+ path = new File(includingFile.getParentFile(), value);
+ }
+ return Optional.of(path);
+ }
+
+ /**
+ * Resolves a {@code core.excludesFile} value.
+ * @param value the raw value of the {@code core.excludesFile} key.
+ * @return the configured file or an empty Optional if it can not be
resolved.
+ */
+ private Optional<File> resolveExcludesFile(final String value) {
+ if (isHomePrefixed(value)) {
+ return expandHome(value);
+ }
+ if (value.startsWith("~")) {
+ return Optional.empty();
+ }
+ File path = new File(value);
+ if (!path.isAbsolute()) {
+ // git resolves relative paths against the current working
directory
+ path = new File(cwd, value);
+ }
+ return Optional.of(path);
+ }
+
+ /**
+ * Expands a {@code ~/} prefixed value against the home directory.
+ * @param value the value to expand.
+ * @return the expanded file or an empty Optional if no home directory is
defined.
+ */
+ private boolean isHomePrefixed(final String value) {
+ return value.startsWith(HOME_PREFIX) ||
value.startsWith(HOME_BACKSLASH_PREFIX);
+ }
+
+ private Optional<File> expandHome(final String value) {
+ if (home == null || home.isEmpty()) {
+ return Optional.empty();
+ }
+ String rest = value.startsWith(HOME_PREFIX) ?
value.substring(HOME_PREFIX.length())
+ : value.substring(HOME_BACKSLASH_PREFIX.length());
+ return Optional.of(new File(home, rest));
+ }
+
+ /**
+ * Counts the number of trailing backslashes of a line.
+ * @param line the line to check.
+ * @return the number of trailing backslashes.
+ */
+ private int trailingBackslashes(final String line) {
Review Comment:
This method is not necessary with the use of the INIConfiguration.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]