/** * This file is part of git-as-svn. It is subject to the license terms * in the LICENSE file found in the top-level directory of this distribution * and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn, * including this file, may be copied, modified, propagated, or distributed * except according to the terms contained in the LICENSE file. */ package svnserver.repository.git.prop; import org.eclipse.jgit.errors.InvalidPatternException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tmatesoft.svn.core.SVNProperty; import svnserver.repository.git.path.PathMatcher; import svnserver.repository.git.path.Wildcard; import java.util.*; import java.util.regex.PatternSyntaxException; /** * Parse and processing .gitignore. * * @author Artem V. Navrotskiy <bozaro@users.noreply.github.com> */ final class GitIgnore implements GitProperty { @NotNull private static final Logger log = LoggerFactory.getLogger(GitIgnore.class); @NotNull private final List<PathMatcher> matchers; // svn:global-ignores @NotNull private final String[] global; // svn:ignore @NotNull private final String[] local; /** * Parse and store .gitignore data (http://git-scm.com/docs/gitignore). * <p> * Important: * * An optional prefix "!" which negates the pattern is not supported. * * Mask trailing slash is not supported (/foo/bar/ works like /foo/bar). * * @param content Original file content. */ public GitIgnore(@NotNull String content) { final List<String> localList = new ArrayList<>(); final List<String> globalList = new ArrayList<>(); matchers = new ArrayList<>(); for (String rawLine : content.split("\n")) { final String line = trimLine(rawLine); if (line.isEmpty()) continue; try { final Wildcard wildcard = new Wildcard(line); if (wildcard.isSvnCompatible()) { processMatcher(localList, globalList, matchers, wildcard.getMatcher()); } } catch (InvalidPatternException | PatternSyntaxException e) { log.warn("Found invalid git pattern: {}", line); } } local = localList.toArray(new String[localList.size()]); global = globalList.toArray(new String[globalList.size()]); } private GitIgnore(@NotNull List<String> local, @NotNull List<String> global, @NotNull List<PathMatcher> matchers) { this.local = local.toArray(new String[local.size()]); this.global = global.toArray(new String[global.size()]); this.matchers = matchers; } private static void processMatcher(@NotNull List<String> local, @NotNull List<String> global, @NotNull List<PathMatcher> matchers, @Nullable PathMatcher matcher) { if (matcher == null) { return; } final String maskGlobal = matcher.getSvnMaskGlobal(); if (maskGlobal != null) { global.add(maskGlobal); return; } final String maskLocal = matcher.getSvnMaskLocal(); if (maskLocal != null) { local.add(maskLocal); } matchers.add(matcher); } private String trimLine(@NotNull String line) { if (line.isEmpty() || line.startsWith("#") || line.startsWith("!") || line.startsWith("\\!")) return ""; // Remove trailing spaces end escapes. int end = line.length(); while (end > 0) { final char c = line.charAt(end - 1); if (c != ' ') { if ((end < line.length()) && (line.charAt(end - 1) == '\\')) { end++; } break; } end--; } return line.substring(0, end); } @Override public void apply(@NotNull Map<String, String> props) { if (global.length > 0) { props.compute(SVNProperty.INHERITABLE_IGNORES, (key, value) -> addIgnore(value, global)); } if (local.length > 0) { props.compute(SVNProperty.IGNORE, (key, value) -> addIgnore(value, local)); } } @Nullable @Override public String getFilterName() { return null; } private static String addIgnore(@Nullable String oldValue, @NotNull String[] ignores) { final Set<String> contains = new HashSet<>(); final StringBuilder result = new StringBuilder(); if (oldValue != null) { result.append(oldValue); contains.addAll(Arrays.asList(oldValue.split("\n"))); } for (String ignore : ignores) { if (contains.add(ignore)) { result.append(ignore).append('\n'); } } return result.toString(); } @Nullable @Override public GitProperty createForChild(@NotNull String name, @NotNull FileMode fileMode) { if (matchers.isEmpty() || (fileMode.getObjectType() == Constants.OBJ_BLOB)) { return null; } final List<String> localList = new ArrayList<>(); final List<String> globalList = new ArrayList<>(); final List<PathMatcher> childMatchers = new ArrayList<>(); for (PathMatcher matcher : matchers) { processMatcher(localList, globalList, childMatchers, matcher.createChild(name, true)); } if (localList.isEmpty() && globalList.isEmpty() && childMatchers.isEmpty()) { return null; } return new GitIgnore(localList, globalList, childMatchers); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; GitIgnore gitIgnore = (GitIgnore) o; return Arrays.equals(global, gitIgnore.global) && Arrays.equals(local, gitIgnore.local) && matchers.equals(gitIgnore.matchers); } @Override public int hashCode() { int result = matchers.hashCode(); result = 31 * result + Arrays.hashCode(global); result = 31 * result + Arrays.hashCode(local); return result; } }