package git.path; import git.path.matcher.name.ComplexMatcher; import git.path.matcher.name.EqualsMatcher; import git.path.matcher.name.RecursiveMatcher; import git.path.matcher.name.SimpleMatcher; import git.path.matcher.path.AlwaysMatcher; import git.path.matcher.path.RecursivePathMatcher; import org.eclipse.jgit.errors.InvalidPatternException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; /** * Git wildcard mask. * <p> * Pattern format: http://git-scm.com/docs/gitignore * * @author Artem V. Navrotskiy <[email protected]> */ public class WildcardHelper { public static final char PATH_SEPARATOR = '/'; @Nullable public static PathMatcher createMatcher(@NotNull String pattern, boolean exact) throws InvalidPatternException { final NameMatcher[] nameMatchers = createNameMatchers(pattern); if (nameMatchers.length > 0) { return new RecursivePathMatcher(nameMatchers, exact); } else { return exact ? null : AlwaysMatcher.INSTANCE; } } private static NameMatcher[] createNameMatchers(@NotNull String pattern) throws InvalidPatternException { final List<String> tokens = WildcardHelper.splitPattern(pattern); WildcardHelper.normalizePattern(tokens); final NameMatcher[] result = new NameMatcher[tokens.size() - 1]; for (int i = 0; i < result.length; ++i) { result[i] = WildcardHelper.nameMatcher(tokens.get(i + 1)); } return result; } @NotNull private static NameMatcher nameMatcher(@NotNull String mask) throws InvalidPatternException { if (mask.equals("**/")) { return RecursiveMatcher.INSTANCE; } final boolean dirOnly = mask.endsWith("/"); final String nameMask = tryRemoveBackslashes(dirOnly ? mask.substring(0, mask.length() - 1) : mask); if ((nameMask.indexOf('[') < 0) && (nameMask.indexOf(']') < 0) && (nameMask.indexOf('\\') < 0)) { // Subversion compatible mask. if (nameMask.indexOf('?') < 0) { int asterisk = nameMask.indexOf('*'); if (asterisk < 0) { return new EqualsMatcher(nameMask, dirOnly); } else if (mask.indexOf('*', asterisk + 1) < 0) { return new SimpleMatcher(nameMask.substring(0, asterisk), nameMask.substring(asterisk + 1), dirOnly); } } return new ComplexMatcher(nameMask, dirOnly, true); } else { return new ComplexMatcher(nameMask, dirOnly, false); } } @NotNull static String tryRemoveBackslashes(@NotNull String pattern) { final StringBuilder result = new StringBuilder(pattern.length()); int start = 0; while (true) { int next = pattern.indexOf('\\', start); if (next == -1) { if (start < pattern.length()) { result.append(pattern, start, pattern.length()); } break; } if (next == pattern.length() - 1) { // Return original string. return pattern; } switch (pattern.charAt(next + 1)) { case ' ': case '#': case '!': result.append(pattern, start, next); start = next + 1; break; default: return pattern; } } return result.toString(); } /** * Split pattern with saving slashes. * * @param pattern Path pattern. * @return Path pattern items. */ @NotNull public static List<String> splitPattern(@NotNull String pattern) { final List<String> result = new ArrayList<>(count(pattern, PATH_SEPARATOR) + 1); int start = 0; while (true) { int next = pattern.indexOf(PATH_SEPARATOR, start); if (next == -1) { if (start < pattern.length()) { result.add(pattern.substring(start)); } break; } result.add(pattern.substring(start, next + 1)); start = next + 1; } return result; } /** * Remove redundant pattern parts and make patterns more simple. * * @param tokens Original modifiable list. * @return Return tokens, */ @NotNull public static List<String> normalizePattern(@NotNull List<String> tokens) { // By default without slashes using mask for files in all subdirectories if ((tokens.size() == 1) && !tokens.get(0).startsWith("/")) { tokens.add(0, "**/"); } // Normalized pattern always starts with "/" if (tokens.size() == 0 || !tokens.get(0).equals("/")) { tokens.add(0, "/"); } // Replace: // * "**/*/" to "*/**/" // * "**/**/" to "**/" // * "**.foo" to "**/*.foo" int index = 1; while (index < tokens.size()) { final String thisToken = tokens.get(index); final String prevToken = tokens.get(index - 1); if (thisToken.equals("/")) { tokens.remove(index); continue; } if (thisToken.equals("**/") && prevToken.equals("**/")) { tokens.remove(index); continue; } if ((!thisToken.equals("**/")) && thisToken.startsWith("**")) { tokens.add(index, "**/"); tokens.set(index + 1, thisToken.substring(1)); continue; } if (thisToken.equals("*/") && prevToken.equals("**/")) { tokens.set(index - 1, "*/"); tokens.set(index, "**/"); index--; continue; } index++; } return tokens; } private static int count(@NotNull String s, char c) { int start = 0; int count = 0; while (true) { start = s.indexOf(c, start); if (start == -1) break; count++; start++; } return count; } public static boolean isMatch(@Nullable PathMatcher matcher, @NotNull String fileName) { List<String> items = splitPattern(fileName.substring(1)); PathMatcher m = matcher; for (String item : items) { if (m == null) break; final boolean dir = item.endsWith(String.valueOf(PATH_SEPARATOR)); m = m.createChild(dir ? item.substring(0, item.length() - 1) : item, dir); } return (m != null) && m.isMatch(); } }