changeset 414:bb278ccf9866

Pull changes from smartgit3 branch
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 21 Mar 2012 20:51:12 +0100
parents 7f27122011c3 (current diff) 63c5a9d7ca3f (diff)
children ee8264d80747
files src/org/tmatesoft/hg/internal/DataAccessProvider.java src/org/tmatesoft/hg/internal/Internals.java src/org/tmatesoft/hg/repo/HgInternals.java src/org/tmatesoft/hg/repo/HgManifest.java src/org/tmatesoft/hg/repo/HgRemoteRepository.java src/org/tmatesoft/hg/repo/HgRepository.java
diffstat 16 files changed, 461 insertions(+), 109 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/internal/BasicSessionContext.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/BasicSessionContext.java	Wed Mar 21 20:51:12 2012 +0100
@@ -32,7 +32,7 @@
 public class BasicSessionContext implements SessionContext {
 
 	private PathPool pathPool;
-	private final LogFacility logFacility;
+	private LogFacility logFacility;
 	private final Map<String, Object> properties;
 	
 	public BasicSessionContext(PathPool pathFactory, LogFacility log) {
@@ -42,7 +42,7 @@
 	@SuppressWarnings("unchecked")
 	public BasicSessionContext(Map<String,?> propertyOverrides, PathPool pathFactory, LogFacility log) {
 		pathPool = pathFactory;
-		logFacility = log != null ? log : new StreamLogFacility(true, true, true, System.out);
+		logFacility = log;
 		properties = propertyOverrides == null ? Collections.<String,Object>emptyMap() : (Map<String, Object>) propertyOverrides;
 	}
 
@@ -55,10 +55,24 @@
 
 	public LogFacility getLog() {
 		// e.g. for exceptions that we can't handle but log (e.g. FileNotFoundException when we've checked beforehand file.canRead()
+		if (logFacility == null) {
+			boolean needDebug = _getBooleanProperty("hg.consolelog.debug", false);
+			boolean needInfo = needDebug || _getBooleanProperty("hg.consolelog.info", false);
+			logFacility = new StreamLogFacility(needDebug, needInfo, true, System.out);
+		}
 		return logFacility;
 	}
+	
+	private boolean _getBooleanProperty(String name, boolean defaultValue) {
+		// can't use <T> and unchecked cast because got no confidence passed properties are strictly of the kind of my default values,
+		// i.e. if boolean from outside comes as "true", while I pass default as Boolean or vice versa.  
+		Object p = getProperty(name, defaultValue);
+		return p instanceof Boolean ? ((Boolean) p).booleanValue() : Boolean.parseBoolean(String.valueOf(p));
+	}
 
+	// TODO specific helpers for boolean and int values
 	public Object getProperty(String name, Object defaultValue) {
+		// NOTE, this method is invoked from getLog(), hence do not call getLog from here unless changed appropriately
 		Object value = properties.get(name);
 		if (value != null) {
 			return value;
--- a/src/org/tmatesoft/hg/internal/DataAccessProvider.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/DataAccessProvider.java	Wed Mar 21 20:51:12 2012 +0100
@@ -237,7 +237,7 @@
 		@Override
 		public void seek(int offset) throws IOException {
 			if (offset > size) {
-				throw new IllegalArgumentException();
+				throw new IllegalArgumentException(String.format("Can't seek to %d for the file of size %d (buffer start:%d)", offset, size, bufferStartInFile));
 			}
 			if (offset < bufferStartInFile + buffer.limit() && offset >= bufferStartInFile) {
 				buffer.position((int) (offset - bufferStartInFile));
--- a/src/org/tmatesoft/hg/internal/EncodingHelper.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/EncodingHelper.java	Wed Mar 21 20:51:12 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
@@ -16,9 +16,11 @@
  */
 package org.tmatesoft.hg.internal;
 
-import java.io.UnsupportedEncodingException;
-
-import org.tmatesoft.hg.core.HgBadStateException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
 
 /**
  * Keep all encoding-related issues in the single place
@@ -27,13 +29,33 @@
  */
 public class EncodingHelper {
 	// XXX perhaps, shall not be full of statics, but rather an instance coming from e.g. HgRepository?
+	/*
+	 * To understand what Mercurial thinks of UTF-8 and Unix byte approach to names, see
+	 * http://mercurial.808500.n3.nabble.com/Unicode-support-request-td3430704.html
+	 */
+	
+	private final CharsetEncoder encoder;
+	private final CharsetDecoder decoder;
+	
+	EncodingHelper(Charset fsEncoding) {
+		decoder = fsEncoding.newDecoder();
+		encoder = fsEncoding.newEncoder();
+	}
 
-	public static String fromManifest(byte[] data, int start, int length) {
+	public String fromManifest(byte[] data, int start, int length) {
 		try {
-			return new String(data, start, length, "ISO-8859-1");
-		} catch (UnsupportedEncodingException ex) {
-			// can't happen
-			throw new HgBadStateException(ex);
+			return decoder.decode(ByteBuffer.wrap(data, start, length)).toString();
+		} catch (CharacterCodingException ex) {
+			// resort to system-default
+			return new String(data, start, length);
 		}
 	}
+
+	public String fromDirstate(byte[] data, int start, int length) throws CharacterCodingException {
+		return decoder.decode(ByteBuffer.wrap(data, start, length)).toString();
+	}
+
+	public Charset charset() {
+		return encoder.charset();
+	}
 }
--- a/src/org/tmatesoft/hg/internal/Internals.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/Internals.java	Wed Mar 21 20:51:12 2012 +0100
@@ -21,9 +21,11 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.StringTokenizer;
@@ -55,13 +57,30 @@
 	 */
 	public static final String CFG_PROPERTY_REVLOG_STREAM_CACHE = "hg4j.repo.disable_revlog_cache";
 	
+	/**
+	 * Name of charset to use when translating Unicode filenames to Mercurial storage paths, string, 
+	 * to resolve with {@link Charset#forName(String)}.
+	 * E.g. <code>"cp1251"</code> or <code>"Latin-1"</code>.
+	 * 
+	 * <p>Mercurial uses system encoding when mangling storage paths. Default value
+	 * based on 'file.encoding' Java system property is usually fine here, however
+	 * in certain scenarios it may be desirable to force a different one, and this 
+	 * property is exactly for this purpose.
+	 * 
+	 * <p>E.g. Eclipse defaults to project encoding (Launch config, Common page) when launching an application, 
+	 * and if your project happen to use anything but filesystem default (say, UTF8 on cp1251 system),
+	 * native storage paths won't match
+	 */
+	public static final String CFG_PROPERTY_FS_FILENAME_ENCODING = "hg.fs.filename.encoding";
+	
 	private int requiresFlags = 0;
 	private List<Filter.Factory> filterFactories;
+	private final SessionContext sessionContext;
 	private final boolean isCaseSensitiveFileSystem;
 	private final boolean shallCacheRevlogsInRepo;
-	
 
 	public Internals(SessionContext ctx) {
+		this.sessionContext = ctx;
 		isCaseSensitiveFileSystem = !runningOnWindows();
 		Object p = ctx.getProperty(CFG_PROPERTY_REVLOG_STREAM_CACHE, true);
 		shallCacheRevlogsInRepo = p instanceof Boolean ? ((Boolean) p).booleanValue() : Boolean.parseBoolean(String.valueOf(p));
@@ -82,18 +101,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? 
 		}
@@ -101,7 +109,10 @@
 
 	// XXX perhaps, should keep both fields right here, not in the HgRepository
 	public PathRewrite buildDataFilesHelper() {
-		return new StoragePathHelper((requiresFlags & STORE) != 0, (requiresFlags & FNCACHE) != 0, (requiresFlags & DOTENCODE) != 0);
+		// Note, tests in TestStorePath depend on the encoding not being cached
+		Charset cs = getFileEncoding();
+		// StoragePathHelper needs fine-grained control over char encoding, hence doesn't use EncodingHelper
+		return new StoragePathHelper((requiresFlags & STORE) != 0, (requiresFlags & FNCACHE) != 0, (requiresFlags & DOTENCODE) != 0, cs);
 	}
 
 	public PathRewrite buildRepositoryFilesHelper() {
@@ -156,6 +167,28 @@
 	public boolean isCaseSensitiveFileSystem() {
 		return isCaseSensitiveFileSystem;
 	}
+	
+	public EncodingHelper buildFileNameEncodingHelper() {
+		return new EncodingHelper(getFileEncoding());
+	}
+	
+	private Charset getFileEncoding() {
+		Object altEncoding = sessionContext.getProperty(CFG_PROPERTY_FS_FILENAME_ENCODING, null);
+		Charset cs;
+		if (altEncoding == null) {
+			cs = Charset.defaultCharset();
+		} else {
+			try {
+				cs = Charset.forName(altEncoding.toString());
+			} catch (IllegalArgumentException ex) {
+				// both IllegalCharsetNameException and UnsupportedCharsetException are subclasses of IAE, too
+				// not severe enough to throw an exception, imo. Just record the fact it's bad ad we ignore it 
+				sessionContext.getLog().error(Internals.class, ex, String.format("Bad configuration value for filename encoding %s", altEncoding));
+				cs = Charset.defaultCharset();
+			}
+		}
+		return cs;
+	}
 
 	public static boolean runningOnWindows() {
 		return System.getProperty("os.name").indexOf("Windows") != -1;
@@ -343,4 +376,22 @@
 	public boolean shallCacheRevlogs() {
 		return shallCacheRevlogsInRepo;
 	}
+	
+	public static <T> CharSequence join(Iterable<T> col, CharSequence separator) {
+		if (col == null) {
+			return String.valueOf(col);
+		}
+		Iterator<T> it = col.iterator();
+		if (!it.hasNext()) {
+			return "[]";
+		}
+		String v = String.valueOf(it.next());
+		StringBuilder sb = new StringBuilder(v);
+		while (it.hasNext()) {
+			sb.append(separator);
+			v = String.valueOf(it.next());
+			sb.append(v);
+		}
+		return sb;
+	}
 }
--- a/src/org/tmatesoft/hg/internal/StoragePathHelper.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/StoragePathHelper.java	Wed Mar 21 20:51:12 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
@@ -16,14 +16,21 @@
  */
 package org.tmatesoft.hg.internal;
 
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
 import java.util.Arrays;
 import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.tmatesoft.hg.util.PathRewrite;
 
 /**
  * @see http://mercurial.selenic.com/wiki/CaseFoldingPlan
  * @see http://mercurial.selenic.com/wiki/fncacheRepoFormat
+ * @see http://mercurial.selenic.com/wiki/EncodingStrategy
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
@@ -33,11 +40,25 @@
 	private final boolean store;
 	private final boolean fncache;
 	private final boolean dotencode;
+	private final Pattern suffix2replace;
+	private final CharsetEncoder csEncoder;
+	private final char[] hexEncodedByte = new char[] {'~', '0', '0'};
+	private final ByteBuffer byteEncodingBuf;
+	private final CharBuffer charEncodingBuf;
+	
+	public StoragePathHelper(boolean isStore, boolean isFncache, boolean isDotencode) {
+		this(isStore, isFncache, isDotencode, Charset.defaultCharset());
+	}
 
-	public StoragePathHelper(boolean isStore, boolean isFncache, boolean isDotencode) {
+	public StoragePathHelper(boolean isStore, boolean isFncache, boolean isDotencode, Charset fsEncoding) {
+		assert fsEncoding != null;
 		store = isStore;
 		fncache = isFncache;
 		dotencode = isDotencode;
+		suffix2replace = Pattern.compile("\\.([id]|hg)/");
+		csEncoder = fsEncoding.newEncoder(); // FIXME catch exception and rethrow as our's RT
+		byteEncodingBuf = ByteBuffer.allocate(Math.round(csEncoder.maxBytesPerChar()) + 1/*in fact, need ceil, hence +1*/);
+		charEncodingBuf = CharBuffer.allocate(1);
 	}
 
 	// FIXME document what path argument is, whether it includes .i or .d, and whether it's 'normalized' (slashes) or not.
@@ -48,13 +69,24 @@
 		final String STR_DATA = "data/";
 		final String STR_DH = "dh/";
 		final String reservedChars = "\\:*?\"<>|";
-		char[] hexByte = new char[2];
 		
-		String path = p.toString();
-		path = path.replace(".hg/", ".hg.hg/").replace(".i/", ".i.hg/").replace(".d/", ".d.hg/");
+		Matcher suffixMatcher = suffix2replace.matcher(p);
+		CharSequence path;
+		// Matcher.replaceAll, but without extra toString
+		boolean found = suffixMatcher.find();
+		if (found) {
+			StringBuffer sb = new StringBuffer(p.length()  + 20);
+			do {
+				suffixMatcher.appendReplacement(sb, ".$1.hg/");
+			} while (found = suffixMatcher.find());
+			suffixMatcher.appendTail(sb);
+			path = sb;
+		} else {
+			path = p;
+		}
+		
 		StringBuilder sb = new StringBuilder(path.length() << 1);
 		if (store || fncache) {
-			// encodefilename
 			for (int i = 0; i < path.length(); i++) {
 				final char ch = path.charAt(i);
 				if (ch >= 'a' && ch <= 'z') {
@@ -63,16 +95,24 @@
 					sb.append('_');
 					sb.append(Character.toLowerCase(ch)); // Perhaps, (char) (((int) ch) + 32)? Even better, |= 0x20? 
 				} else if (reservedChars.indexOf(ch) != -1) {
-					sb.append('~');
-					sb.append(toHexByte(ch, hexByte));
+					sb.append(toHexByte(ch));
 				} else if ((ch >= '~' /*126*/ && ch <= 255) || ch < ' ' /*32*/) {
-					sb.append('~');
-					sb.append(toHexByte(ch, hexByte));
+					sb.append(toHexByte(ch));
 				} else if (ch == '_') {
 					sb.append('_');
 					sb.append('_');
 				} else {
-					sb.append(ch);
+					// either ASCII char that doesn't require special handling, or an Unicode character to get encoded
+					// according to filesystem/native encoding, see http://mercurial.selenic.com/wiki/EncodingStrategy
+					// despite of what the page says, use of native encoding seems worst solution to me (repositories
+					// can't be easily shared between OS'es with different encodings then, e.g. Win1251 and Linux UTF8).
+					// If the ease of sharing was not the point, what's the reason to mangle with names at all then (
+					// lowercase and exclude reserved device names).
+					if (ch < '~' /*126*/ || !csEncoder.canEncode(ch)) {
+						sb.append(ch);
+					} else {
+						appendEncoded(sb, ch);
+					}
 				}
 			}
 			// auxencode
@@ -82,6 +122,9 @@
 		}
 		final int MAX_PATH_LEN = 120;
 		if (fncache && (sb.length() + STR_DATA.length() + ".i".length() > MAX_PATH_LEN)) {
+			// TODO [post-1.0] Mercurial uses system encoding for paths, hence we need to pass bytes to DigestHelper
+			// to ensure our sha1 value (default encoding of unicode string if one looks into DH impl) match that 
+			// produced by Mercurial (based on native string). 
 			String digest = new DigestHelper().sha1(STR_DATA, path, ".i").asHexString();
 			final int DIR_PREFIX_LEN = 8;
 			 // not sure why (-4) is here. 120 - 40 = up to 80 for path with ext. dh/ + ext(.i) = 3+2
@@ -94,13 +137,15 @@
 				} else if (ch >= 'A' && ch <= 'Z') {
 					sb.append((char) (ch | 0x20)); // lowercase 
 				} else if (reservedChars.indexOf(ch) != -1) {
-					sb.append('~');
-					sb.append(toHexByte(ch, hexByte));
+					sb.append(toHexByte(ch));
 				} else if ((ch >= '~' /*126*/ && ch <= 255) || ch < ' ' /*32*/) {
-					sb.append('~');
-					sb.append(toHexByte(ch, hexByte));
+					sb.append(toHexByte(ch));
 				} else {
-					sb.append(ch);
+					if (ch < '~' /*126*/ || !csEncoder.canEncode(ch)) {
+						sb.append(ch);
+					} else {
+						appendEncoded(sb, ch);
+					}
 				}
 			}
 			encodeWindowsDeviceNames(sb);
@@ -163,7 +208,6 @@
 	}
 	
 	private void encodeWindowsDeviceNames(StringBuilder sb) {
-		char[] hexByte = new char[2];
 		int x = 0; // last segment start
 		final TreeSet<String> windowsReservedFilenames = new TreeSet<String>();
 		windowsReservedFilenames.addAll(Arrays.asList("con prn aux nul com1 com2 com3 com4 com5 com6 com7 com8 com9 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9".split(" "))); 
@@ -183,25 +227,49 @@
 					found = windowsReservedFilenames.contains(sb.subSequence(x, x+4));
 				}
 				if (found) {
-					sb.insert(x+3, toHexByte(sb.charAt(x+2), hexByte));
-					sb.setCharAt(x+2, '~');
+					// x+2 as we change the third letter in device name
+					replace(sb, x+2, toHexByte(sb.charAt(x+2)));
 					i += 2;
 				}
 			}
 			if (dotencode && (sb.charAt(x) == '.' || sb.charAt(x) == ' ')) {
-				sb.insert(x+1, toHexByte(sb.charAt(x), hexByte));
-				sb.setCharAt(x, '~'); // setChar *after* charAt/insert to get ~2e, not ~7e for '.'
+				char dotOrSpace = sb.charAt(x); // beware, replace() below changes charAt(x), rather get a copy 
+				// not to get ~7e for '.' instead of ~2e, if later refactoring changes the logic 
+				replace(sb, x, toHexByte(dotOrSpace));
 				i += 2;
 			}
 			x = i+1;
 		} while (x < sb.length());
 	}
+	
+	// shall be synchronized in case of multithreaded use
+	private void appendEncoded(StringBuilder sb, char ch) {
+		charEncodingBuf.clear();
+		byteEncodingBuf.clear();
+		charEncodingBuf.put(ch).flip();
+		csEncoder.encode(charEncodingBuf, byteEncodingBuf, false);
+		byteEncodingBuf.flip();
+		while (byteEncodingBuf.hasRemaining()) {
+			sb.append(toHexByte(byteEncodingBuf.get()));
+		}
+	}
 
-	private static char[] toHexByte(int ch, char[] buf) {
-		assert buf.length > 1;
+	/**
+	 * replace char at sb[index] with a sequence
+	 */
+	private static void replace(StringBuilder sb, int index, char[] with) {
+		// there's StringBuilder.replace(int, int+1, String), but with char[] - I don't want to make a string out of hexEncodedByte
+		sb.setCharAt(index, with[0]);
+		sb.insert(index+1, with, 1, with.length - 1);
+	}
+
+	/**
+	 * put hex representation of byte ch into buf from specified offset 
+	 */
+	private char[] toHexByte(int ch) {
 		final String hexDigits = "0123456789abcdef";
-		buf[0] = hexDigits.charAt((ch & 0x00F0) >>> 4);
-		buf[1] = hexDigits.charAt(ch & 0x0F);
-		return buf;
+		hexEncodedByte[1] = hexDigits.charAt((ch & 0x00F0) >>> 4);
+		hexEncodedByte[2] = hexDigits.charAt(ch & 0x0F);
+		return hexEncodedByte;
 	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/WinToNixPathRewrite.java	Wed Mar 21 20:51:12 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/HgDirstate.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgDirstate.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2011 TMate Software Ltd
+ * Copyright (c) 2010-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
@@ -23,6 +23,7 @@
 import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.io.IOException;
+import java.nio.charset.CharacterCodingException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -32,6 +33,7 @@
 import org.tmatesoft.hg.core.HgInvalidControlFileException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.internal.DataAccess;
+import org.tmatesoft.hg.internal.EncodingHelper;
 import org.tmatesoft.hg.util.Pair;
 import org.tmatesoft.hg.util.Path;
 import org.tmatesoft.hg.util.PathPool;
@@ -74,7 +76,7 @@
 		canonicalPathRewrite = canonicalPath;
 	}
 
-	/*package-local*/ void read() throws HgInvalidControlFileException {
+	/*package-local*/ void read(EncodingHelper encodingHelper) throws HgInvalidControlFileException {
 		normal = added = removed = merged = Collections.<Path, Record>emptyMap();
 		parents = new Pair<Nodeid,Nodeid>(Nodeid.NULL, Nodeid.NULL);
 		if (canonicalPathRewrite != null) {
@@ -108,13 +110,13 @@
 				da.readBytes(name, 0, nameLen);
 				for (int i = 0; i < nameLen; i++) {
 					if (name[i] == 0) {
-						fn1 = new String(name, 0, i, "UTF-8"); // XXX unclear from documentation what encoding is used there
-						fn2 = new String(name, i+1, nameLen - i - 1, "UTF-8"); // need to check with different system codepages
+						fn1 = encodingHelper.fromDirstate(name, 0, i);
+						fn2 = encodingHelper.fromDirstate(name, i+1, nameLen - i - 1);
 						break;
 					}
 				}
 				if (fn1 == null) {
-					fn1 = new String(name);
+					fn1 = encodingHelper.fromDirstate(name, 0, nameLen);
 				}
 				Record r = new Record(fmode, size, time, pathPool.path(fn1), fn2 == null ? null : pathPool.path(fn2));
 				if (canonicalPathRewrite != null) {
@@ -145,6 +147,8 @@
 					repo.getContext().getLog().warn(getClass(), "Dirstate record for file %s (size: %d, tstamp:%d) has unknown state '%c'", r.name1, r.size(), r.time, state);
 				}
 			}
+		} catch (CharacterCodingException ex) {
+			throw new HgInvalidControlFileException(String.format("Failed reading file names from dirstate using encoding %s", encodingHelper.charset().name()), ex, dirstateFile);
 		} catch (IOException ex) {
 			throw new HgInvalidControlFileException("Dirstate read failed", ex, dirstateFile); 
 		} finally {
--- a/src/org/tmatesoft/hg/repo/HgIgnore.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgIgnore.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2011 TMate Software Ltd
+ * Copyright (c) 2010-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
@@ -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,16 +57,18 @@
 		}
 	}
 
-	/* 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>();
 		ArrayList<Pattern> result = new ArrayList<Pattern>(entries); // start with existing
-		String syntax = "regexp"; // or "glob"
+		String syntax = REGEXP;
 		String line;
 		while ((line = content.readLine()) != null) {
 			line = line.trim();
 			if (line.startsWith("syntax:")) {
 				syntax = line.substring("syntax:".length()).trim();
-				if (!"regexp".equals(syntax) && !"glob".equals(syntax)) {
+				if (!REGEXP.equals(syntax) && !GLOB.equals(syntax)) {
 					errors.add(line);
 					continue;
 					//throw new IllegalStateException(line);
@@ -81,17 +86,32 @@
 						line = line.substring(0, x).trim();
 					}
 				}
+				// due to the nature of Mercurial implementation, lines prefixed with syntax kind
+				// are processed correctly (despite the fact hgignore(5) suggest "syntax:<kind>" as the 
+				// only way to specify it). lineSyntax below leaves a chance for the line to switch 
+				// syntax in use without affecting default kind.
+				String lineSyntax;
+				if (line.startsWith(GLOB_PREFIX)) {
+					line = line.substring(GLOB_PREFIX.length()).trim();
+					lineSyntax = GLOB;
+				} else if (line.startsWith(REGEXP_PREFIX)) {
+					line = line.substring(REGEXP_PREFIX.length()).trim();
+					lineSyntax = REGEXP;
+				} else {
+					lineSyntax = syntax;
+				}
 				if (line.length() == 0) {
 					continue;
 				}
-				if ("glob".equals(syntax)) {
-					// 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
+				if (GLOB.equals(lineSyntax)) {
+					// 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(syntax);
+					assert REGEXP.equals(lineSyntax);
 					// regular expression patterns need not match start of the line unless demanded explicitly 
 					line = line.charAt(0) == '^' ? line : ".*" + line;
 				}
@@ -160,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	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgInternals.java	Wed Mar 21 20:51:12 2012 +0100
@@ -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;
@@ -71,7 +73,7 @@
 			};
 		}
 		HgDirstate ds = new HgDirstate(repo, new File(repo.getRepositoryRoot(), "dirstate"), new PathPool(new PathRewrite.Empty()), canonicalPath);
-		ds.read();
+		ds.read(repo.getImplHelper().buildFileNameEncodingHelper());
 		return ds;
 	}
 	
@@ -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/HgManifest.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgManifest.java	Wed Mar 21 20:51:12 2012 +0100
@@ -52,6 +52,7 @@
  */
 public class HgManifest extends Revlog {
 	private RevisionMapper revisionMap;
+	private EncodingHelper encodingHelper;
 	
 	public enum Flags {
 		Exec, Link; // FIXME REVISIT consider REGULAR instead of null
@@ -96,8 +97,9 @@
 		}
 	}
 
-	/*package-local*/ HgManifest(HgRepository hgRepo, RevlogStream content) {
+	/*package-local*/ HgManifest(HgRepository hgRepo, RevlogStream content, EncodingHelper eh) {
 		super(hgRepo, content);
+		encodingHelper = eh;
 	}
 
 	/**
@@ -174,7 +176,7 @@
 			manifestLast = manifestFirst;
 			manifestFirst = x;
 		}
-		content.iterate(manifestFirst, manifestLast, true, new ManifestParser(inspector));
+		content.iterate(manifestFirst, manifestLast, true, new ManifestParser(inspector, encodingHelper));
 	}
 	
 	/**
@@ -194,7 +196,7 @@
 			throw new IllegalArgumentException();
 		}
 		int[] manifestRevs = toManifestRevisionIndexes(revisionIndexes, inspector);
-		content.iterate(manifestRevs, true, new ManifestParser(inspector));
+		content.iterate(manifestRevs, true, new ManifestParser(inspector, encodingHelper));
 	}
 	
 	// 
@@ -345,11 +347,13 @@
 		private int start; 
 		private final int hash, length;
 		private Path result;
+		private final EncodingHelper encHelper;
 
-		public PathProxy(byte[] data, int start, int length) {
+		public PathProxy(byte[] data, int start, int length, EncodingHelper eh) {
 			this.data = data;
 			this.start = start;
 			this.length = length;
+			this.encHelper = eh;
 
 			// copy from String.hashCode(). In fact, not necessarily match result of String(data).hashCode
 			// just need some nice algorithm here
@@ -387,7 +391,7 @@
 		
 		public Path freeze() {
 			if (result == null) {
-				result = Path.create(EncodingHelper.fromManifest(data, start, length));
+				result = Path.create(encHelper.fromManifest(data, start, length));
 				// release reference to bigger data array, make a copy of relevant part only
 				// use original bytes, not those from String above to avoid cache misses due to different encodings 
 				byte[] d = new byte[length];
@@ -407,11 +411,13 @@
 		private byte[] nodeidLookupBuffer = new byte[20]; // get reassigned each time new Nodeid is added to pool
 		private final ProgressSupport progressHelper;
 		private IterateControlMediator iterateControl;
+		private final EncodingHelper encHelper;
 		
-		public ManifestParser(Inspector delegate) {
+		public ManifestParser(Inspector delegate, EncodingHelper eh) {
 			assert delegate != null;
 			inspector = delegate;
 			inspector2 = delegate instanceof Inspector2 ? (Inspector2) delegate : null;
+			encHelper = eh;
 			nodeidPool = new Pool2<Nodeid>();
 			fnamePool = new Pool2<PathProxy>();
 			thisRevPool = new Pool2<Nodeid>();
@@ -435,7 +441,7 @@
 						int x = i;
 						for( ; data[i] != '\n' && i < actualLen; i++) {
 							if (fname == null && data[i] == 0) {
-								PathProxy px = fnamePool.unify(new PathProxy(data, x, i - x));
+								PathProxy px = fnamePool.unify(new PathProxy(data, x, i - x, encHelper));
 								// if (cached = fnamePool.unify(px))== px then cacheMiss, else cacheHit
 								// cpython 0..10k: hits: 15 989 152, misses: 3020
 								fname = px.freeze();
--- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Wed Mar 21 20:51:12 2012 +0100
@@ -66,7 +66,7 @@
 	private final URL url;
 	private final SSLContext sslContext;
 	private final String authInfo;
-	private final boolean debug = Boolean.parseBoolean(System.getProperty("hg4j.remote.debug"));
+	private final boolean debug;
 	private HgLookup lookupHelper;
 	private final SessionContext sessionContext;
 
@@ -76,6 +76,8 @@
 		}
 		this.url = url;
 		sessionContext = ctx;
+		Object debugProp = ctx.getProperty("hg4j.remote.debug", false);
+		debug = debugProp instanceof Boolean ? ((Boolean) debugProp).booleanValue() : Boolean.parseBoolean(String.valueOf(debugProp));
 		if ("https".equals(url.getProtocol())) {
 			try {
 				sslContext = SSLContext.getInstance("SSL");
--- a/src/org/tmatesoft/hg/repo/HgRepository.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgRepository.java	Wed Mar 21 20:51:12 2012 +0100
@@ -35,6 +35,7 @@
 import org.tmatesoft.hg.internal.DataAccessProvider;
 import org.tmatesoft.hg.internal.Experimental;
 import org.tmatesoft.hg.internal.Filter;
+import org.tmatesoft.hg.internal.Internals;
 import org.tmatesoft.hg.internal.RevlogStream;
 import org.tmatesoft.hg.internal.SubrepoManager;
 import org.tmatesoft.hg.util.CancelledException;
@@ -169,7 +170,7 @@
 	public HgManifest getManifest() {
 		if (manifest == null) {
 			RevlogStream content = resolve(Path.create(repoPathHelper.rewrite("00manifest.i")), true);
-			manifest = new HgManifest(this, content);
+			manifest = new HgManifest(this, content, impl.buildFileNameEncodingHelper());
 		}
 		return manifest;
 	}
@@ -332,7 +333,7 @@
 			};
 		}
 		HgDirstate ds = new HgDirstate(this, new File(repoDir, "dirstate"), pathPool, canonicalPath);
-		ds.read();
+		ds.read(impl.buildFileNameEncodingHelper());
 		return ds;
 	}
 
@@ -343,12 +344,12 @@
 	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);
 				if (errors != null) {
-					getContext().getLog().warn(getClass(), "Syntax errors parsing .hgignore:\n%s", errors);
+					getContext().getLog().warn(getClass(), "Syntax errors parsing .hgignore:\n%s", Internals.join(errors, ",\n"));
 				}
 			} catch (IOException ex) {
 				final String m = "Error reading .hgignore file";
@@ -409,6 +410,10 @@
 	/*package-local*/ SessionContext getContext() {
 		return sessionContext;
 	}
+	
+	/*package-local*/ Internals getImplHelper() {
+		return impl;
+	}
 
 	private List<Filter> instantiateFilters(Path p, Filter.Options opts) {
 		List<Filter.Factory> factories = impl.getFilters(this);
--- a/test/org/tmatesoft/hg/test/OutputParser.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/test/org/tmatesoft/hg/test/OutputParser.java	Wed Mar 21 20:51:12 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
@@ -27,6 +27,8 @@
 
 	public class Stub implements OutputParser {
 		private boolean shallDump;
+		private CharSequence result;
+
 		public Stub() {
 			this(false);
 		}
@@ -34,10 +36,14 @@
 			shallDump = dump;
 		}
 		public void parse(CharSequence seq) {
+			result = seq;
 			if (shallDump) {
 				System.out.println(seq);
 			} 
 			// else no-op
 		}
+		public CharSequence result() {
+			return result;
+		}
 	}
 }
--- a/test/org/tmatesoft/hg/test/TestDirstate.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/test/org/tmatesoft/hg/test/TestDirstate.java	Wed Mar 21 20:51:12 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
@@ -66,7 +66,11 @@
 		repo = Configuration.get().find("log-branches");
 		Assert.assertEquals("test", repo.getWorkingCopyBranchName());
 		repo = Configuration.get().own();
-		Assert.assertEquals("default", repo.getWorkingCopyBranchName());
+		OutputParser.Stub output = new OutputParser.Stub();
+		ExecHelper eh = new ExecHelper(output, repo.getWorkingDir());
+		eh.run("hg", "branch");
+		String branchName = output.result().toString().trim();
+		Assert.assertEquals(branchName, repo.getWorkingCopyBranchName());
 	}
 
 	@Test
--- a/test/org/tmatesoft/hg/test/TestIgnore.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/test/org/tmatesoft/hg/test/TestIgnore.java	Wed Mar 21 20:51:12 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
@@ -16,6 +16,8 @@
  */
 package org.tmatesoft.hg.test;
 
+import static org.tmatesoft.hg.util.Path.create;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
@@ -23,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;
@@ -49,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));
@@ -59,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"),
@@ -68,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"),
@@ -85,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");
@@ -102,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");
@@ -121,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"),
@@ -145,11 +145,63 @@
 				Path.create("cd/x"),
 				Path.create("cxyd/x"),
 		};
-		for (Path p : toIgnore) {
-			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), null);
+		Path[] toPass = new Path[] {
+				create("a/c"),
+				create("a/d"),
+				create("a/d.a"),
+				create("a/d.e"),
+		};
+		Path[] toIgnore = new Path[] {
+				create("a.c"),
+				create("a.d"),
+				create("src/a.c"),
+				create("src/a.d"),
+		};
+		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(p.toString(), !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));
+			}
 		}
 	}
 }
--- a/test/org/tmatesoft/hg/test/TestStorePath.java	Wed Mar 21 20:40:28 2012 +0100
+++ b/test/org/tmatesoft/hg/test/TestStorePath.java	Wed Mar 21 20:51:12 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
@@ -16,9 +16,13 @@
  */
 package org.tmatesoft.hg.test;
 
+import java.util.HashMap;
+import java.util.Map;
+
 import junit.framework.Assert;
 
 import org.hamcrest.CoreMatchers;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.tmatesoft.hg.internal.BasicSessionContext;
@@ -36,18 +40,30 @@
 	public ErrorCollectorExt errorCollector = new ErrorCollectorExt();
 	
 	private PathRewrite storePathHelper;
+	private final Map<String, Object> propertyOverrides = new HashMap<String, Object>();
+
+	private Internals internals;
 
 	public static void main(String[] args) throws Throwable {
 		final TestStorePath test = new TestStorePath();
 		test.testWindowsFilenames();
 		test.testHashLongPath();
+		test.testSuffixReplacement();
 		test.errorCollector.verify();
 	}
 	
 	public TestStorePath() {
-		final Internals i = new Internals(new BasicSessionContext(null, null));
-		i.setStorageConfig(1, 0x7);
-		storePathHelper = i.buildDataFilesHelper();
+		propertyOverrides.put("hg.consolelog.debug", true);
+		internals = new Internals(new BasicSessionContext(propertyOverrides, null, null));
+		internals.setStorageConfig(1, 0x7);
+		storePathHelper = internals.buildDataFilesHelper();
+	}
+	
+	@Before
+	public void setup() {
+		// just in case there are leftovers from #testNationalChars() and another test builds a helper
+		propertyOverrides.clear();
+		propertyOverrides.put("hg.consolelog.debug", true);
 	}
 
 	@Test
@@ -88,4 +104,27 @@
 		errorCollector.checkThat(storePathHelper.rewrite(n2), CoreMatchers.<CharSequence>equalTo(r2));
 		errorCollector.checkThat(storePathHelper.rewrite(n3), CoreMatchers.<CharSequence>equalTo(r3));
 	}
+
+	@Test
+	public void testSuffixReplacement() {
+		String s1 = "aaa/bbb.hg/ccc.i/ddd.hg/xx.i";
+		String s2 = "bbb.d/aa.hg/bbb.hg/yy.d";
+		String r1 = s1.replace(".hg/", ".hg.hg/").replace(".i/", ".i.hg/").replace(".d/", ".d.hg/");
+		String r2 = s2.replace(".hg/", ".hg.hg/").replace(".i/", ".i.hg/").replace(".d/", ".d.hg/");
+		errorCollector.checkThat(storePathHelper.rewrite(s1), CoreMatchers.<CharSequence>equalTo("store/data/" + r1 + ".i"));
+		errorCollector.checkThat(storePathHelper.rewrite(s2), CoreMatchers.<CharSequence>equalTo("store/data/" + r2 + ".i"));
+	}
+	
+	@Test
+	public void testNationalChars() {
+		String s = "Привет.txt";
+		//
+		propertyOverrides.put(Internals.CFG_PROPERTY_FS_FILENAME_ENCODING, "cp1251");
+		PathRewrite sph = internals.buildDataFilesHelper();
+		errorCollector.checkThat(sph.rewrite(s), CoreMatchers.<CharSequence>equalTo("store/data/~cf~f0~e8~e2~e5~f2.txt.i"));
+		//
+		propertyOverrides.put(Internals.CFG_PROPERTY_FS_FILENAME_ENCODING, "UTF8");
+		sph = internals.buildDataFilesHelper();
+		errorCollector.checkThat(sph.rewrite(s), CoreMatchers.<CharSequence>equalTo("store/data/~d0~9f~d1~80~d0~b8~d0~b2~d0~b5~d1~82.txt.i"));
+	}
 }