[ignore rules] fix for backslash handling
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>
This commit is contained in:
parent
4b7daecf4a
commit
1abd51d953
|
@ -829,22 +829,30 @@ public void testEscapedBracket6() throws Exception {
|
||||||
assertMatch("[a\\]]", "a", true);
|
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
|
@Test
|
||||||
public void testEscapedBackslash() throws Exception {
|
public void testEscapedBackslash() throws Exception {
|
||||||
// In Git CLI a\\b matches a\b file
|
// In Git CLI a\\b matches a\b file
|
||||||
assertMatch("a\\\\b", "a\\b", true);
|
assertMatch("a\\\\b", "a\\b", true);
|
||||||
|
assertMatch("a\\\\b\\c", "a\\bc", true);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEscapedExclamationMark() throws Exception {
|
public void testEscapedExclamationMark() throws Exception {
|
||||||
assertMatch("\\!b!.txt", "!b!.txt", true);
|
assertMatch("\\!b!.txt", "!b!.txt", true);
|
||||||
assertMatch("a\\!b!.txt", "a\\!b!.txt", true);
|
assertMatch("a\\!b!.txt", "a!b!.txt", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEscapedHash() throws Exception {
|
public void testEscapedHash() throws Exception {
|
||||||
assertMatch("\\#b", "#b", true);
|
assertMatch("\\#b", "#b", true);
|
||||||
assertMatch("a\\#", "a\\#", true);
|
assertMatch("a\\#", "a#", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -855,12 +863,12 @@ public void testEscapedTrailingSpaces() throws Exception {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNotEscapingBackslash() throws Exception {
|
public void testNotEscapingBackslash() throws Exception {
|
||||||
assertMatch("\\out", "\\out", true);
|
assertMatch("\\out", "out", true);
|
||||||
assertMatch("\\out", "a/\\out", true);
|
assertMatch("\\out", "a/out", true);
|
||||||
assertMatch("c:\\/", "c:\\/", true);
|
assertMatch("c:\\/", "c:/", true);
|
||||||
assertMatch("c:\\/", "a/c:\\/", true);
|
assertMatch("c:\\/", "a/c:/", true);
|
||||||
assertMatch("c:\\tmp", "c:\\tmp", true);
|
assertMatch("c:\\tmp", "c:tmp", true);
|
||||||
assertMatch("c:\\tmp", "a/c:\\tmp", true);
|
assertMatch("c:\\tmp", "a/c:tmp", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -868,6 +876,22 @@ public void testMultipleEscapedCharacters1() throws Exception {
|
||||||
assertMatch("\\]a?c\\*\\[d\\?\\]", "]abc*[d?]", true);
|
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
|
@Test
|
||||||
public void testDollar() throws Exception {
|
public void testDollar() throws Exception {
|
||||||
assertMatch("$", "$", true);
|
assertMatch("$", "$", true);
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
public class LeadingAsteriskMatcher extends NameMatcher {
|
public class LeadingAsteriskMatcher extends NameMatcher {
|
||||||
|
|
||||||
LeadingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
|
LeadingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
|
||||||
super(pattern, pathSeparator, dirOnly);
|
super(pattern, pathSeparator, dirOnly, true);
|
||||||
|
|
||||||
if (subPattern.charAt(0) != '*')
|
if (subPattern.charAt(0) != '*')
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
|
|
|
@ -58,9 +58,13 @@ public class NameMatcher extends AbstractMatcher {
|
||||||
|
|
||||||
final String subPattern;
|
final String subPattern;
|
||||||
|
|
||||||
NameMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
|
NameMatcher(String pattern, Character pathSeparator, boolean dirOnly,
|
||||||
|
boolean deleteBackslash) {
|
||||||
super(pattern, dirOnly);
|
super(pattern, dirOnly);
|
||||||
slash = getPathSeparator(pathSeparator);
|
slash = getPathSeparator(pathSeparator);
|
||||||
|
if (deleteBackslash) {
|
||||||
|
pattern = Strings.deleteBackslash(pattern);
|
||||||
|
}
|
||||||
beginning = pattern.length() == 0 ? false : pattern.charAt(0) == slash;
|
beginning = pattern.length() == 0 ? false : pattern.charAt(0) == slash;
|
||||||
if (!beginning)
|
if (!beginning)
|
||||||
this.subPattern = pattern;
|
this.subPattern = pattern;
|
||||||
|
|
|
@ -85,7 +85,8 @@ public class PathMatcher extends AbstractMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSimplePathWithSegments(String path) {
|
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,
|
static private List<IMatcher> createMatchers(List<String> segments,
|
||||||
|
@ -167,7 +168,7 @@ private static IMatcher createNameMatcher0(String segment,
|
||||||
case COMPLEX:
|
case COMPLEX:
|
||||||
return new WildCardMatcher(segment, pathSeparator, dirOnly);
|
return new WildCardMatcher(segment, pathSeparator, dirOnly);
|
||||||
default:
|
default:
|
||||||
return new NameMatcher(segment, pathSeparator, dirOnly);
|
return new NameMatcher(segment, pathSeparator, dirOnly, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,9 +157,7 @@ private static boolean isComplexWildcard(String pattern) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
char nextChar = pattern.charAt(nextIdx);
|
char nextChar = pattern.charAt(nextIdx);
|
||||||
if (nextChar == '?' || nextChar == '*' || nextChar == '['
|
if (escapedByBackslash(nextChar)) {
|
||||||
// required to match escaped backslashes '\\\\'
|
|
||||||
|| nextChar == '\\') {
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
@ -169,6 +167,10 @@ private static boolean isComplexWildcard(String pattern) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean escapedByBackslash(char nextChar) {
|
||||||
|
return nextChar == '?' || nextChar == '*' || nextChar == '[';
|
||||||
|
}
|
||||||
|
|
||||||
static PatternState checkWildCards(String pattern) {
|
static PatternState checkWildCards(String pattern) {
|
||||||
if (isComplexWildcard(pattern))
|
if (isComplexWildcard(pattern))
|
||||||
return PatternState.COMPLEX;
|
return PatternState.COMPLEX;
|
||||||
|
@ -308,6 +310,14 @@ && isLetter(lookAhead(pattern, i)))
|
||||||
char lookAhead = lookAhead(pattern, i);
|
char lookAhead = lookAhead(pattern, i);
|
||||||
if (lookAhead == ']' || lookAhead == '[')
|
if (lookAhead == ']' || lookAhead == '[')
|
||||||
ignoreLastBracket = true;
|
ignoreLastBracket = true;
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
char lookAhead = lookAhead(pattern, i);
|
||||||
|
if (lookAhead != '\\' && lookAhead != '['
|
||||||
|
&& lookAhead != '?' && lookAhead != '*'
|
||||||
|
&& lookAhead != ' ' && lookBehind(sb) != '\\') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sb.append(c);
|
sb.append(c);
|
||||||
break;
|
break;
|
||||||
|
@ -445,4 +455,30 @@ private static String checkPosixCharClass(char[] buffer) {
|
||||||
return null;
|
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 @@
|
||||||
public class TrailingAsteriskMatcher extends NameMatcher {
|
public class TrailingAsteriskMatcher extends NameMatcher {
|
||||||
|
|
||||||
TrailingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
|
TrailingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
|
||||||
super(pattern, pathSeparator, dirOnly);
|
super(pattern, pathSeparator, dirOnly, true);
|
||||||
|
|
||||||
if (subPattern.charAt(subPattern.length() - 1) != '*')
|
if (subPattern.charAt(subPattern.length() - 1) != '*')
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
|
|
|
@ -62,7 +62,7 @@ public class WildCardMatcher extends NameMatcher {
|
||||||
|
|
||||||
WildCardMatcher(String pattern, Character pathSeparator, boolean dirOnly)
|
WildCardMatcher(String pattern, Character pathSeparator, boolean dirOnly)
|
||||||
throws InvalidPatternException {
|
throws InvalidPatternException {
|
||||||
super(pattern, pathSeparator, dirOnly);
|
super(pattern, pathSeparator, dirOnly, false);
|
||||||
p = convertGlob(subPattern);
|
p = convertGlob(subPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue