Browse Source

[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>
stable-4.1
Andrey Loskutov 9 years ago
parent
commit
1abd51d953
  1. 40
      org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java
  2. 2
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java
  3. 6
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java
  4. 5
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java
  5. 42
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java
  6. 2
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java
  7. 2
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java

40
org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java

@ -829,22 +829,30 @@ public class IgnoreRuleSpecialCasesTest {
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 class IgnoreRuleSpecialCasesTest {
@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 class IgnoreRuleSpecialCasesTest {
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);

2
org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java

@ -50,7 +50,7 @@ package org.eclipse.jgit.ignore.internal;
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(

6
org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java

@ -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;

5
org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java

@ -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 @@ public class PathMatcher extends AbstractMatcher {
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);
} }
} }

42
org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java

@ -157,9 +157,7 @@ public class Strings {
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 @@ public class Strings {
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 @@ public class Strings {
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 @@ public class Strings {
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();
}
} }

2
org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java

@ -50,7 +50,7 @@ package org.eclipse.jgit.ignore.internal;
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(

2
org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java

@ -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…
Cancel
Save