Mercurial > hg4j
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")); + } }