An attempt to re-implement not well documented Git CLI behavior for patterns with backslashes. It looks like Git silently ignores all \ characters in ignore rules, if they are NOT covered by 3 cases described in [1]: {quote} 1) ... Put a backslash ("\") in front of the first hash for patterns that begin with a hash. ... 2) Trailing spaces are ignored unless they are quoted with backslash ("\"). ... 3) Put a backslash ("\") in front of the first "!" for patterns that begin with a literal "!", for example, "\!important!.txt". {quote} Undocumented also is the fact that backslash itself can be escaped by backslash. So \h\e\l\l\o\.t\x\t rule matches hello.txt and a\\\\b a\b in Git CLI. Additionally, the glob parser [2] knows special meaning of backslash: {quote} One can remove the special meaning of '?', '*' and '[' by preceding them by a backslash, or, in case this is part of a shell command line, enclosing them in quotes. Between brackets these characters stand for themselves. Thus, "[[?*\]" matches the four characters '[', '?', '*' and '\'. {quote} [1] https://www.kernel.org/pub/software/scm/git/docs/gitignore.html [2] http://man7.org/linux/man-pages/man7/glob.7.html Bug: 478065 Change-Id: I3dc973475d1943c5622103701fa8cb3ea0684e3e Signed-off-by: Andrey Loskutov <loskutov@gmx.de>tags/v4.1.0.201509280440-r
@@ -829,22 +829,30 @@ public class IgnoreRuleSpecialCasesTest { | |||
assertMatch("[a\\]]", "a", true); | |||
} | |||
@Test | |||
public void testIgnoredBackslash() throws Exception { | |||
// In Git CLI a\b\c is equal to abc | |||
assertMatch("a\\b\\c", "abc", true); | |||
} | |||
@Test | |||
public void testEscapedBackslash() throws Exception { | |||
// In Git CLI a\\b matches a\b file | |||
assertMatch("a\\\\b", "a\\b", true); | |||
assertMatch("a\\\\b\\c", "a\\bc", true); | |||
} | |||
@Test | |||
public void testEscapedExclamationMark() throws Exception { | |||
assertMatch("\\!b!.txt", "!b!.txt", true); | |||
assertMatch("a\\!b!.txt", "a\\!b!.txt", true); | |||
assertMatch("a\\!b!.txt", "a!b!.txt", true); | |||
} | |||
@Test | |||
public void testEscapedHash() throws Exception { | |||
assertMatch("\\#b", "#b", true); | |||
assertMatch("a\\#", "a\\#", true); | |||
assertMatch("a\\#", "a#", true); | |||
} | |||
@Test | |||
@@ -855,12 +863,12 @@ public class IgnoreRuleSpecialCasesTest { | |||
@Test | |||
public void testNotEscapingBackslash() throws Exception { | |||
assertMatch("\\out", "\\out", true); | |||
assertMatch("\\out", "a/\\out", true); | |||
assertMatch("c:\\/", "c:\\/", true); | |||
assertMatch("c:\\/", "a/c:\\/", true); | |||
assertMatch("c:\\tmp", "c:\\tmp", true); | |||
assertMatch("c:\\tmp", "a/c:\\tmp", true); | |||
assertMatch("\\out", "out", true); | |||
assertMatch("\\out", "a/out", true); | |||
assertMatch("c:\\/", "c:/", true); | |||
assertMatch("c:\\/", "a/c:/", true); | |||
assertMatch("c:\\tmp", "c:tmp", true); | |||
assertMatch("c:\\tmp", "a/c:tmp", true); | |||
} | |||
@Test | |||
@@ -868,6 +876,22 @@ public class IgnoreRuleSpecialCasesTest { | |||
assertMatch("\\]a?c\\*\\[d\\?\\]", "]abc*[d?]", true); | |||
} | |||
@Test | |||
public void testBackslash() throws Exception { | |||
assertMatch("a\\", "a", true); | |||
assertMatch("\\a", "a", true); | |||
assertMatch("a/\\", "a/", true); | |||
assertMatch("a/b\\", "a/b", true); | |||
assertMatch("\\a/b", "a/b", true); | |||
assertMatch("/\\a", "/a", true); | |||
assertMatch("\\a\\b\\c\\", "abc", true); | |||
assertMatch("/\\a/\\b/\\c\\", "a/b/c", true); | |||
// empty path segment doesn't match | |||
assertMatch("\\/a", "/a", false); | |||
assertMatch("\\/a", "a", false); | |||
} | |||
@Test | |||
public void testDollar() throws Exception { | |||
assertMatch("$", "$", true); |
@@ -50,7 +50,7 @@ package org.eclipse.jgit.ignore.internal; | |||
public class LeadingAsteriskMatcher extends NameMatcher { | |||
LeadingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) { | |||
super(pattern, pathSeparator, dirOnly); | |||
super(pattern, pathSeparator, dirOnly, true); | |||
if (subPattern.charAt(0) != '*') | |||
throw new IllegalArgumentException( |
@@ -58,9 +58,13 @@ public class NameMatcher extends AbstractMatcher { | |||
final String subPattern; | |||
NameMatcher(String pattern, Character pathSeparator, boolean dirOnly) { | |||
NameMatcher(String pattern, Character pathSeparator, boolean dirOnly, | |||
boolean deleteBackslash) { | |||
super(pattern, dirOnly); | |||
slash = getPathSeparator(pathSeparator); | |||
if (deleteBackslash) { | |||
pattern = Strings.deleteBackslash(pattern); | |||
} | |||
beginning = pattern.length() == 0 ? false : pattern.charAt(0) == slash; | |||
if (!beginning) | |||
this.subPattern = pattern; |
@@ -85,7 +85,8 @@ public class PathMatcher extends AbstractMatcher { | |||
} | |||
private boolean isSimplePathWithSegments(String path) { | |||
return !isWildCard(path) && count(path, slash, true) > 0; | |||
return !isWildCard(path) && path.indexOf('\\') < 0 | |||
&& count(path, slash, true) > 0; | |||
} | |||
static private List<IMatcher> createMatchers(List<String> segments, | |||
@@ -167,7 +168,7 @@ public class PathMatcher extends AbstractMatcher { | |||
case COMPLEX: | |||
return new WildCardMatcher(segment, pathSeparator, dirOnly); | |||
default: | |||
return new NameMatcher(segment, pathSeparator, dirOnly); | |||
return new NameMatcher(segment, pathSeparator, dirOnly, true); | |||
} | |||
} | |||
@@ -157,9 +157,7 @@ public class Strings { | |||
return false; | |||
} | |||
char nextChar = pattern.charAt(nextIdx); | |||
if (nextChar == '?' || nextChar == '*' || nextChar == '[' | |||
// required to match escaped backslashes '\\\\' | |||
|| nextChar == '\\') { | |||
if (escapedByBackslash(nextChar)) { | |||
return true; | |||
} else { | |||
return false; | |||
@@ -169,6 +167,10 @@ public class Strings { | |||
return false; | |||
} | |||
private static boolean escapedByBackslash(char nextChar) { | |||
return nextChar == '?' || nextChar == '*' || nextChar == '['; | |||
} | |||
static PatternState checkWildCards(String pattern) { | |||
if (isComplexWildcard(pattern)) | |||
return PatternState.COMPLEX; | |||
@@ -308,6 +310,14 @@ public class Strings { | |||
char lookAhead = lookAhead(pattern, i); | |||
if (lookAhead == ']' || lookAhead == '[') | |||
ignoreLastBracket = true; | |||
} else { | |||
// | |||
char lookAhead = lookAhead(pattern, i); | |||
if (lookAhead != '\\' && lookAhead != '[' | |||
&& lookAhead != '?' && lookAhead != '*' | |||
&& lookAhead != ' ' && lookBehind(sb) != '\\') { | |||
break; | |||
} | |||
} | |||
sb.append(c); | |||
break; | |||
@@ -445,4 +455,30 @@ public class Strings { | |||
return null; | |||
} | |||
static String deleteBackslash(String s) { | |||
if (s.indexOf('\\') < 0) { | |||
return s; | |||
} | |||
StringBuilder sb = new StringBuilder(s.length()); | |||
for (int i = 0; i < s.length(); i++) { | |||
char ch = s.charAt(i); | |||
if (ch == '\\') { | |||
if (i + 1 == s.length()) { | |||
continue; | |||
} | |||
char next = s.charAt(i + 1); | |||
if (next == '\\') { | |||
sb.append(ch); | |||
i++; | |||
continue; | |||
} | |||
if (!escapedByBackslash(next)) { | |||
continue; | |||
} | |||
} | |||
sb.append(ch); | |||
} | |||
return sb.toString(); | |||
} | |||
} |
@@ -50,7 +50,7 @@ package org.eclipse.jgit.ignore.internal; | |||
public class TrailingAsteriskMatcher extends NameMatcher { | |||
TrailingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) { | |||
super(pattern, pathSeparator, dirOnly); | |||
super(pattern, pathSeparator, dirOnly, true); | |||
if (subPattern.charAt(subPattern.length() - 1) != '*') | |||
throw new IllegalArgumentException( |
@@ -62,7 +62,7 @@ public class WildCardMatcher extends NameMatcher { | |||
WildCardMatcher(String pattern, Character pathSeparator, boolean dirOnly) | |||
throws InvalidPatternException { | |||
super(pattern, pathSeparator, dirOnly); | |||
super(pattern, pathSeparator, dirOnly, false); | |||
p = convertGlob(subPattern); | |||
} | |||