changeset 409:0f5696623512 smartgit3

Support glob path pattern rewrite to facilitate use of globs with Windows path separator
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 16 Mar 2012 20:14:47 +0100
parents e732521a9eb4
children df5009d67be2
files src/org/tmatesoft/hg/internal/Internals.java src/org/tmatesoft/hg/internal/WinToNixPathRewrite.java src/org/tmatesoft/hg/repo/HgIgnore.java src/org/tmatesoft/hg/repo/HgInternals.java src/org/tmatesoft/hg/repo/HgRepository.java test/org/tmatesoft/hg/test/TestIgnore.java
diffstat 6 files changed, 125 insertions(+), 47 deletions(-) [+]
line wrap: on
line diff
--- 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? 
 		}
--- /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
--- 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<Pattern> entries;
+	private final PathRewrite globPathHelper;
 
-	HgIgnore() {
+	HgIgnore(PathRewrite globPathRewrite) {
 		entries = Collections.emptyList();
+		globPathHelper = globPathRewrite;
 	}
 
-	/* package-local */List<String> read(File hgignoreFile) throws IOException {
+	/* package-local */ List<String> read(File hgignoreFile) throws IOException {
 		if (!hgignoreFile.exists()) {
 			return null;
 		}
@@ -54,7 +57,7 @@
 		}
 	}
 
-	/* package-local */List<String> read(BufferedReader content) throws IOException {
+	/* package-local */ List<String> read(BufferedReader content) throws IOException {
 		final String REGEXP = "regexp", GLOB = "glob";
 		final String REGEXP_PREFIX = REGEXP + ":", GLOB_PREFIX = GLOB + ":";
 		ArrayList<String> errors = new ArrayList<String>();
@@ -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();
 	}
 
--- 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 <code>null</code> 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();
--- 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<String> errors = ignore.read(ignoreFile);
--- 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));
+			}
 		}
 	}
 }