diff options
author | Andrey Loskutov <loskutov@gmx.de> | 2015-09-25 00:37:50 +0200 |
---|---|---|
committer | Andrey Loskutov <loskutov@gmx.de> | 2015-09-26 18:16:42 +0200 |
commit | 1abd51d95373fcf7450463478b45aa7da8012cef (patch) | |
tree | 4cd9a68fe658e9b985559443b17fb5b99856da62 | |
parent | 4b7daecf4a827b9cf69a76e5cbd8559e8079f4c8 (diff) | |
download | jgit-1abd51d95373fcf7450463478b45aa7da8012cef.tar.gz jgit-1abd51d95373fcf7450463478b45aa7da8012cef.zip |
[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>
7 files changed, 82 insertions, 17 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java index ec4a1f1c67..f8eb126826 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java @@ -830,21 +830,29 @@ public class IgnoreRuleSpecialCasesTest { } @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 @@ -869,6 +877,22 @@ public class IgnoreRuleSpecialCasesTest { } @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); assertMatch("$x", "$x", true); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java index f1153d9c69..3d0ad09124 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java +++ b/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 { LeadingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) { - super(pattern, pathSeparator, dirOnly); + super(pattern, pathSeparator, dirOnly, true); if (subPattern.charAt(0) != '*') throw new IllegalArgumentException( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java index 6c4c8092fe..8beae8379e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java @@ -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; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java index 6b9b5a806b..c3f6694a7a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java +++ b/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) { - 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); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java index 2e955d3124..f972828bc6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java @@ -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(); + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java index 4a1c780d99..b927d27dbe 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java +++ b/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 { 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( diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java index 7d12b0ddce..8f9815283d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java +++ b/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) throws InvalidPatternException { - super(pattern, pathSeparator, dirOnly); + super(pattern, pathSeparator, dirOnly, false); p = convertGlob(subPattern); } |