Skip to content

Commit 1abd51d

Browse files
committed
[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 <[email protected]>
1 parent 4b7daec commit 1abd51d

File tree

7 files changed

+82
-17
lines changed

7 files changed

+82
-17
lines changed

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

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -829,22 +829,30 @@ public void testEscapedBracket6() throws Exception {
829829
assertMatch("[a\\]]", "a", true);
830830
}
831831

832+
@Test
833+
public void testIgnoredBackslash() throws Exception {
834+
// In Git CLI a\b\c is equal to abc
835+
assertMatch("a\\b\\c", "abc", true);
836+
}
837+
832838
@Test
833839
public void testEscapedBackslash() throws Exception {
834840
// In Git CLI a\\b matches a\b file
835841
assertMatch("a\\\\b", "a\\b", true);
842+
assertMatch("a\\\\b\\c", "a\\bc", true);
843+
836844
}
837845

838846
@Test
839847
public void testEscapedExclamationMark() throws Exception {
840848
assertMatch("\\!b!.txt", "!b!.txt", true);
841-
assertMatch("a\\!b!.txt", "a\\!b!.txt", true);
849+
assertMatch("a\\!b!.txt", "a!b!.txt", true);
842850
}
843851

844852
@Test
845853
public void testEscapedHash() throws Exception {
846854
assertMatch("\\#b", "#b", true);
847-
assertMatch("a\\#", "a\\#", true);
855+
assertMatch("a\\#", "a#", true);
848856
}
849857

850858
@Test
@@ -855,19 +863,35 @@ public void testEscapedTrailingSpaces() throws Exception {
855863

856864
@Test
857865
public void testNotEscapingBackslash() throws Exception {
858-
assertMatch("\\out", "\\out", true);
859-
assertMatch("\\out", "a/\\out", true);
860-
assertMatch("c:\\/", "c:\\/", true);
861-
assertMatch("c:\\/", "a/c:\\/", true);
862-
assertMatch("c:\\tmp", "c:\\tmp", true);
863-
assertMatch("c:\\tmp", "a/c:\\tmp", true);
866+
assertMatch("\\out", "out", true);
867+
assertMatch("\\out", "a/out", true);
868+
assertMatch("c:\\/", "c:/", true);
869+
assertMatch("c:\\/", "a/c:/", true);
870+
assertMatch("c:\\tmp", "c:tmp", true);
871+
assertMatch("c:\\tmp", "a/c:tmp", true);
864872
}
865873

866874
@Test
867875
public void testMultipleEscapedCharacters1() throws Exception {
868876
assertMatch("\\]a?c\\*\\[d\\?\\]", "]abc*[d?]", true);
869877
}
870878

879+
@Test
880+
public void testBackslash() throws Exception {
881+
assertMatch("a\\", "a", true);
882+
assertMatch("\\a", "a", true);
883+
assertMatch("a/\\", "a/", true);
884+
assertMatch("a/b\\", "a/b", true);
885+
assertMatch("\\a/b", "a/b", true);
886+
assertMatch("/\\a", "/a", true);
887+
assertMatch("\\a\\b\\c\\", "abc", true);
888+
assertMatch("/\\a/\\b/\\c\\", "a/b/c", true);
889+
890+
// empty path segment doesn't match
891+
assertMatch("\\/a", "/a", false);
892+
assertMatch("\\/a", "a", false);
893+
}
894+
871895
@Test
872896
public void testDollar() throws Exception {
873897
assertMatch("$", "$", true);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
public class LeadingAsteriskMatcher extends NameMatcher {
5151

5252
LeadingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
53-
super(pattern, pathSeparator, dirOnly);
53+
super(pattern, pathSeparator, dirOnly, true);
5454

5555
if (subPattern.charAt(0) != '*')
5656
throw new IllegalArgumentException(

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,13 @@ public class NameMatcher extends AbstractMatcher {
5858

5959
final String subPattern;
6060

61-
NameMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
61+
NameMatcher(String pattern, Character pathSeparator, boolean dirOnly,
62+
boolean deleteBackslash) {
6263
super(pattern, dirOnly);
6364
slash = getPathSeparator(pathSeparator);
65+
if (deleteBackslash) {
66+
pattern = Strings.deleteBackslash(pattern);
67+
}
6468
beginning = pattern.length() == 0 ? false : pattern.charAt(0) == slash;
6569
if (!beginning)
6670
this.subPattern = pattern;

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ public class PathMatcher extends AbstractMatcher {
8585
}
8686

8787
private boolean isSimplePathWithSegments(String path) {
88-
return !isWildCard(path) && count(path, slash, true) > 0;
88+
return !isWildCard(path) && path.indexOf('\\') < 0
89+
&& count(path, slash, true) > 0;
8990
}
9091

9192
static private List<IMatcher> createMatchers(List<String> segments,
@@ -167,7 +168,7 @@ private static IMatcher createNameMatcher0(String segment,
167168
case COMPLEX:
168169
return new WildCardMatcher(segment, pathSeparator, dirOnly);
169170
default:
170-
return new NameMatcher(segment, pathSeparator, dirOnly);
171+
return new NameMatcher(segment, pathSeparator, dirOnly, true);
171172
}
172173
}
173174

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,7 @@ private static boolean isComplexWildcard(String pattern) {
157157
return false;
158158
}
159159
char nextChar = pattern.charAt(nextIdx);
160-
if (nextChar == '?' || nextChar == '*' || nextChar == '['
161-
// required to match escaped backslashes '\\\\'
162-
|| nextChar == '\\') {
160+
if (escapedByBackslash(nextChar)) {
163161
return true;
164162
} else {
165163
return false;
@@ -169,6 +167,10 @@ private static boolean isComplexWildcard(String pattern) {
169167
return false;
170168
}
171169

170+
private static boolean escapedByBackslash(char nextChar) {
171+
return nextChar == '?' || nextChar == '*' || nextChar == '[';
172+
}
173+
172174
static PatternState checkWildCards(String pattern) {
173175
if (isComplexWildcard(pattern))
174176
return PatternState.COMPLEX;
@@ -308,6 +310,14 @@ && isLetter(lookAhead(pattern, i)))
308310
char lookAhead = lookAhead(pattern, i);
309311
if (lookAhead == ']' || lookAhead == '[')
310312
ignoreLastBracket = true;
313+
} else {
314+
//
315+
char lookAhead = lookAhead(pattern, i);
316+
if (lookAhead != '\\' && lookAhead != '['
317+
&& lookAhead != '?' && lookAhead != '*'
318+
&& lookAhead != ' ' && lookBehind(sb) != '\\') {
319+
break;
320+
}
311321
}
312322
sb.append(c);
313323
break;
@@ -445,4 +455,30 @@ private static String checkPosixCharClass(char[] buffer) {
445455
return null;
446456
}
447457

458+
static String deleteBackslash(String s) {
459+
if (s.indexOf('\\') < 0) {
460+
return s;
461+
}
462+
StringBuilder sb = new StringBuilder(s.length());
463+
for (int i = 0; i < s.length(); i++) {
464+
char ch = s.charAt(i);
465+
if (ch == '\\') {
466+
if (i + 1 == s.length()) {
467+
continue;
468+
}
469+
char next = s.charAt(i + 1);
470+
if (next == '\\') {
471+
sb.append(ch);
472+
i++;
473+
continue;
474+
}
475+
if (!escapedByBackslash(next)) {
476+
continue;
477+
}
478+
}
479+
sb.append(ch);
480+
}
481+
return sb.toString();
482+
}
483+
448484
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
public class TrailingAsteriskMatcher extends NameMatcher {
5151

5252
TrailingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
53-
super(pattern, pathSeparator, dirOnly);
53+
super(pattern, pathSeparator, dirOnly, true);
5454

5555
if (subPattern.charAt(subPattern.length() - 1) != '*')
5656
throw new IllegalArgumentException(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public class WildCardMatcher extends NameMatcher {
6262

6363
WildCardMatcher(String pattern, Character pathSeparator, boolean dirOnly)
6464
throws InvalidPatternException {
65-
super(pattern, pathSeparator, dirOnly);
65+
super(pattern, pathSeparator, dirOnly, false);
6666
p = convertGlob(subPattern);
6767
}
6868

0 commit comments

Comments
 (0)