Java tutorial
/** * 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.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.tmatesoft.svn.core.SVNProperty; import java.io.IOException; import java.util.*; /** * Parse and processing .gitignore. * * @author Artem V. Navrotskiy <bozaro@users.noreply.github.com> */ final class GitIgnore implements GitProperty { @SuppressWarnings("UnusedDeclaration") public static final class Factory implements GitPropertyFactory { @NotNull @Override public String getFileName() { return ".gitignore"; } @NotNull @Override public GitProperty create(@NotNull String content) throws IOException { return new GitIgnore(content); } } @NotNull private final List<Rule> rules; // 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<>(); rules = new ArrayList<>(); for (String rawLine : content.split("\n")) { String line = parseLine(rawLine); if (line.isEmpty()) continue; processLine(localList, globalList, rules, 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<Rule> rules) { this.local = local.toArray(new String[local.size()]); this.global = global.toArray(new String[global.size()]); this.rules = rules; } private static void processLine(@NotNull List<String> localList, @NotNull List<String> globalList, @NotNull List<Rule> rules, @NotNull String ruleLine) { String line = ruleLine; if (line.startsWith("/") && line.indexOf('/', 1) != -1) line = line.substring(1); if (line.startsWith("**/") && line.indexOf('/', 3) == -1) line = line.substring(3); // Remove unusefull prefix final int lastIndex = line.lastIndexOf('/'); if (lastIndex == -1) { // simple mask in all dirs globalList.add(line); return; } else if (lastIndex == 0) { // simple mask in current dir localList.add(line.substring(1)); return; } final int index = line.indexOf('/'); rules.add(new Rule(line.substring(0, index), line.substring(index))); } @NotNull public static String parseLine(@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) { if (line.charAt(end - 1) != ' ') { if ((end < line.length()) && (line.charAt(end - 1) == '\\')) { end++; } break; } end--; } String parsed = line.substring(0, end).replaceAll("\\\\", ""); // Add leading "/" if (!parsed.startsWith("/") && parsed.contains("/")) { parsed = "/" + parsed; } // Remove trailing "/" if (parsed.endsWith("/")) { if (parsed.indexOf('/') == parsed.length() - 1) { // foo/ -> /foo parsed = "/" + parsed.substring(0, parsed.length() - 1); } else { // /foo/bar/ -> /foo/bar // foo/bar/ -> foo/bar parsed = parsed.substring(0, parsed.length() - 1); } } // Remove trailing "/**" while (parsed.endsWith("/**")) { parsed = parsed.substring(0, parsed.length() - 3); } // Remove leading "**/" while (parsed.startsWith("/**/**/")) { parsed = parsed.substring(3); } if (parsed.startsWith("/**/") && (parsed.indexOf('/', 4) == -1)) { parsed = parsed.substring(4); } // Remove leading "/" if (parsed.startsWith("/") && (parsed.indexOf('/', 1) != -1)) { parsed = parsed.substring(1); } // Return. return parsed; } @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)); } } 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 (rules.isEmpty() || (fileMode.getObjectType() == Constants.OBJ_BLOB)) { return null; } final List<String> localList = new ArrayList<>(); final List<String> globalList = new ArrayList<>(); final List<Rule> childRules = new ArrayList<>(); for (Rule rule : rules) { if (rule.mask.equals("**")) { childRules.add(rule); final int index = rule.rule.indexOf('/', 1); if (rule.rule.substring(1, index).equals(name)) { processLine(localList, globalList, childRules, rule.rule.substring(index)); } } if (FilenameUtils.wildcardMatch(name, rule.mask, IOCase.SENSITIVE)) { processLine(localList, globalList, childRules, rule.rule); } } if (localList.isEmpty() && globalList.isEmpty() && childRules.isEmpty()) { return null; } return new GitIgnore(localList, globalList, childRules); } @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) && rules.equals(gitIgnore.rules); } @Override public int hashCode() { int result = rules.hashCode(); result = 31 * result + Arrays.hashCode(global); result = 31 * result + Arrays.hashCode(local); return result; } private final static class Rule { @NotNull private final String mask; @NotNull private final String rule; private Rule(@NotNull String mask, @NotNull String rule) { this.mask = mask; this.rule = rule; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Rule rule1 = (Rule) o; return mask.equals(rule1.mask) && rule.equals(rule1.rule); } @Override public int hashCode() { int result = mask.hashCode(); result = 31 * result + rule.hashCode(); return result; } } }