# HG changeset patch # User Artem Tikhomirov # Date 1331925287 -3600 # Node ID 0f56966235124878d9818e270212355758c9fee7 # Parent e732521a9eb473f25453305b691087815b68205e Support glob path pattern rewrite to facilitate use of globs with Windows path separator diff -r e732521a9eb4 -r 0f5696623512 src/org/tmatesoft/hg/internal/Internals.java --- a/src/org/tmatesoft/hg/internal/Internals.java Fri Mar 16 15:06:44 2012 +0100 +++ b/src/org/tmatesoft/hg/internal/Internals.java Fri Mar 16 20:14:47 2012 +0100 @@ -83,18 +83,7 @@ public PathRewrite buildNormalizePathRewrite() { if (runningOnWindows()) { - return new PathRewrite() { - - public CharSequence rewrite(CharSequence p) { - // TODO handle . and .. (although unlikely to face them from GUI client) - String path = p.toString(); - path = path.replace('\\', '/').replace("//", "/"); - if (path.startsWith("/")) { - path = path.substring(1); - } - return path; - } - }; + return new WinToNixPathRewrite(); } else { return new PathRewrite.Empty(); // or strip leading slash, perhaps? } diff -r e732521a9eb4 -r 0f5696623512 src/org/tmatesoft/hg/internal/WinToNixPathRewrite.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/WinToNixPathRewrite.java Fri Mar 16 20:14:47 2012 +0100 @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2012 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import org.tmatesoft.hg.util.PathRewrite; + +/** + * Translate windows path separators to Unix/POSIX-style + * + * @author Artem Tikhomirov + * @author Tmate Software Ltd. + */ +public final class WinToNixPathRewrite implements PathRewrite { + public CharSequence rewrite(CharSequence p) { + // TODO handle . and .. (although unlikely to face them from GUI client) + String path = p.toString(); + path = path.replace('\\', '/').replace("//", "/"); + if (path.startsWith("/")) { + path = path.substring(1); + } + return path; + } +} \ No newline at end of file diff -r e732521a9eb4 -r 0f5696623512 src/org/tmatesoft/hg/repo/HgIgnore.java --- a/src/org/tmatesoft/hg/repo/HgIgnore.java Fri Mar 16 15:06:44 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/HgIgnore.java Fri Mar 16 20:14:47 2012 +0100 @@ -27,6 +27,7 @@ import java.util.regex.PatternSyntaxException; import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.PathRewrite; /** * Handling of ignored paths according to .hgignore configuration @@ -37,12 +38,14 @@ public class HgIgnore implements Path.Matcher { private List entries; + private final PathRewrite globPathHelper; - HgIgnore() { + HgIgnore(PathRewrite globPathRewrite) { entries = Collections.emptyList(); + globPathHelper = globPathRewrite; } - /* package-local */List read(File hgignoreFile) throws IOException { + /* package-local */ List read(File hgignoreFile) throws IOException { if (!hgignoreFile.exists()) { return null; } @@ -54,7 +57,7 @@ } } - /* package-local */List read(BufferedReader content) throws IOException { + /* package-local */ List read(BufferedReader content) throws IOException { final String REGEXP = "regexp", GLOB = "glob"; final String REGEXP_PREFIX = REGEXP + ":", GLOB_PREFIX = GLOB + ":"; ArrayList errors = new ArrayList(); @@ -101,10 +104,11 @@ continue; } if (GLOB.equals(lineSyntax)) { - // hgignore(5) - // (http://www.selenic.com/mercurial/hgignore.5.html) says slashes '\' are escape characters, - // hence no special treatment of Windows path - // however, own attempts make me think '\' on Windows are not treated as escapes + // hgignore(5) says slashes '\' are escape characters, + // however, for glob patterns on Windows first get backslashes converted to slashes + if (globPathHelper != null) { + line = globPathHelper.rewrite(line).toString(); + } line = glob2regex(line); } else { assert REGEXP.equals(lineSyntax); @@ -176,7 +180,13 @@ } sb.append(ch); } - sb.append("(?:/|$)"); + // Python impl doesn't keep empty segments in directory names (ntpath.normpath and posixpath.normpath), + // effectively removing trailing separators, thus patterns like "bin/" get translated into "bin$" + // Our glob rewriter doesn't strip last empty segment, and "bin/$" would be incorrect pattern, + // (e.g. isIgnored("bin/file") performs two matches, against "bin/file" and "bin") hence the check. + if (sb.charAt(sb.length() - 1) != '/') { + sb.append('$'); + } return sb.toString(); } diff -r e732521a9eb4 -r 0f5696623512 src/org/tmatesoft/hg/repo/HgInternals.java --- a/src/org/tmatesoft/hg/repo/HgInternals.java Fri Mar 16 15:06:44 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/HgInternals.java Fri Mar 16 20:14:47 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 TMate Software Ltd * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,7 +29,9 @@ import org.tmatesoft.hg.core.HgInvalidRevisionException; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.Experimental; +import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.RelativePathRewrite; +import org.tmatesoft.hg.internal.WinToNixPathRewrite; import org.tmatesoft.hg.util.FileIterator; import org.tmatesoft.hg.util.FileWalker; import org.tmatesoft.hg.util.Path; @@ -87,8 +89,22 @@ return hgRepo.getRepositoryRoot(); } - public static HgIgnore newHgIgnore(Reader source) throws IOException { - HgIgnore hgIgnore = new HgIgnore(); + /** + * @param source where to read definitions from + * @param globPathRewrite null to use default, or pass an instance to override defaults + * @return + * @throws IOException + */ + public static HgIgnore newHgIgnore(Reader source, PathRewrite globPathRewrite) throws IOException { + if (globPathRewrite == null) { + // shall match that of HgRepository#getIgnore() (Internals#buildNormalizePathRewrite()) + if (Internals.runningOnWindows()) { + globPathRewrite = new WinToNixPathRewrite(); + } else { + globPathRewrite = new PathRewrite.Empty(); + } + } + HgIgnore hgIgnore = new HgIgnore(globPathRewrite); BufferedReader br = source instanceof BufferedReader ? (BufferedReader) source : new BufferedReader(source); hgIgnore.read(br); br.close(); diff -r e732521a9eb4 -r 0f5696623512 src/org/tmatesoft/hg/repo/HgRepository.java --- a/src/org/tmatesoft/hg/repo/HgRepository.java Fri Mar 16 15:06:44 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/HgRepository.java Fri Mar 16 20:14:47 2012 +0100 @@ -319,7 +319,7 @@ public HgIgnore getIgnore() /*throws HgInvalidControlFileException */{ // TODO read config for additional locations if (ignore == null) { - ignore = new HgIgnore(); + ignore = new HgIgnore(getToRepoPathHelper()); File ignoreFile = new File(getWorkingDir(), ".hgignore"); try { final List errors = ignore.read(ignoreFile); diff -r e732521a9eb4 -r 0f5696623512 test/org/tmatesoft/hg/test/TestIgnore.java --- a/test/org/tmatesoft/hg/test/TestIgnore.java Fri Mar 16 15:06:44 2012 +0100 +++ b/test/org/tmatesoft/hg/test/TestIgnore.java Fri Mar 16 20:14:47 2012 +0100 @@ -25,6 +25,7 @@ import org.junit.Rule; import org.junit.Test; +import org.tmatesoft.hg.internal.WinToNixPathRewrite; import org.tmatesoft.hg.repo.HgIgnore; import org.tmatesoft.hg.repo.HgInternals; import org.tmatesoft.hg.util.Path; @@ -51,7 +52,7 @@ @Test public void testGlobWithAlternatives() throws Exception { String content = "syntax:glob\ndoc/*.[0-9].{x,ht}ml"; - HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(content)); + HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(content), null); final Path p1 = Path.create("doc/asd.2.xml"); final Path p2 = Path.create("doc/zxc.6.html"); errorCollector.assertTrue(p1.toString(), hgIgnore.isIgnored(p1)); @@ -61,7 +62,7 @@ @Test public void testComplexFileParse() throws Exception { BufferedReader br = new BufferedReader(new FileReader(new File(Configuration.get().getTestDataDir(), "mercurial.hgignore"))); - HgIgnore hgIgnore = HgInternals.newHgIgnore(br); + HgIgnore hgIgnore = HgInternals.newHgIgnore(br, null); br.close(); Path[] toCheck = new Path[] { Path.create("file.so"), @@ -70,15 +71,13 @@ Path.create(".#abc"), Path.create("locale/en/LC_MESSAGES/hg.mo"), }; - for (Path p : toCheck) { - errorCollector.assertTrue(p.toString(), hgIgnore.isIgnored(p)); - } + doAssert(hgIgnore, toCheck, null); } @Test public void testSegmentsGlobMatch() throws Exception { String s = "syntax:glob\nbin\n.*\nTEST-*.xml"; - HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s)); + HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s), null); Path[] toCheck = new Path[] { Path.create("bin/org/sample/First.class"), Path.create(".ignored-file"), @@ -87,11 +86,10 @@ Path.create("TEST-a.xml"), Path.create("dir/TEST-b.xml"), }; - for (Path p : toCheck) { - errorCollector.assertTrue(p.toString(), hgIgnore.isIgnored(p)); - } + doAssert(hgIgnore, toCheck, null); + // s = "syntax:glob\n.git"; - hgIgnore = HgInternals.newHgIgnore(new StringReader(s)); + hgIgnore = HgInternals.newHgIgnore(new StringReader(s), null); Path p = Path.create(".git/aa"); errorCollector.assertTrue(p.toString(), hgIgnore.isIgnored(p)); p = Path.create("dir/.git/bb"); @@ -104,7 +102,7 @@ public void testSegmentsRegexMatch() throws Exception { // regex patterns that don't start with explicit ^ are allowed to match anywhere in the string String s = "syntax:regexp\n/\\.git\n^abc\n"; - HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s)); + HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s), null); Path p = Path.create(".git/aa"); errorCollector.assertTrue(p.toString(), !hgIgnore.isIgnored(p)); p = Path.create("dir/.git/bb"); @@ -123,7 +121,7 @@ @Test public void testWildcardsDoNotMatchDirectorySeparator() throws Exception { String s = "syntax:glob\na?b\nc*d"; - HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s)); + HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s), null); // shall not be ignored Path[] toPass = new Path[] { Path.create("a/b"), @@ -147,18 +145,13 @@ Path.create("cd/x"), Path.create("cxyd/x"), }; - for (Path p : toIgnore) { - errorCollector.assertTrue(p.toString(), hgIgnore.isIgnored(p)); - } - for (Path p : toPass) { - errorCollector.assertTrue(p.toString(), !hgIgnore.isIgnored(p)); - } + doAssert(hgIgnore, toIgnore, toPass); } @Test public void testSyntaxPrefixAtLine() throws Exception { String s = "glob:*.c\nregexp:.*\\.d"; - HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s)); + HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s), null); Path[] toPass = new Path[] { create("a/c"), create("a/d"), @@ -171,11 +164,44 @@ create("src/a.c"), create("src/a.d"), }; - for (Path p : toIgnore) { - errorCollector.assertTrue("Shall ignore " + p, hgIgnore.isIgnored(p)); + doAssert(hgIgnore, toIgnore, toPass); + } + + @Test + public void testGlobWithWindowsPathSeparators() throws Exception { + String s = "syntax:glob\n" + "bin\\*\n" + "*\\dir*\\*.a\n" + "*\\_ReSharper*\\\n"; + // explicit PathRewrite for the test to run on *nix as well + HgIgnore hgIgnore = HgInternals.newHgIgnore(new StringReader(s), new WinToNixPathRewrite()); + Path[] toPass = new Path[] { + create("bind/x"), + create("dir/x.a"), + create("dir-b/x.a"), + create("a/dir-b/x.b"), + create("_ReSharper-1/file"), + }; + Path[] toIgnore = new Path[] { +// create("bin/x"), +// create("a/bin/x"), +// create("a/dir/c.a"), +// create("b/dir-c/d.a"), + create("src/_ReSharper-1/file/x"), + }; + doAssert(hgIgnore, toIgnore, toPass); + } + + private void doAssert(HgIgnore hgIgnore, Path[] toIgnore, Path[] toPass) { + if (toIgnore == null && toPass == null) { + throw new IllegalArgumentException(); } - for (Path p : toPass) { - errorCollector.assertTrue("Shall pass " + p, !hgIgnore.isIgnored(p)); + if (toIgnore != null) { + for (Path p : toIgnore) { + errorCollector.assertTrue("Shall ignore " + p, hgIgnore.isIgnored(p)); + } + } + if (toPass != null) { + for (Path p : toPass) { + errorCollector.assertTrue("Shall pass " + p, !hgIgnore.isIgnored(p)); + } } } }