changeset 114:46291ec605a0

Filters to read and initialize according to configuration files
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 03 Feb 2011 22:13:55 +0100
parents 67ae317408c9
children c0cc2535462c
files TODO cmdline/org/tmatesoft/hg/console/Remote.java src/org/tmatesoft/hg/internal/ConfigFile.java src/org/tmatesoft/hg/internal/Filter.java src/org/tmatesoft/hg/internal/Internals.java src/org/tmatesoft/hg/internal/KeywordFilter.java src/org/tmatesoft/hg/internal/NewlineFilter.java src/org/tmatesoft/hg/internal/PathGlobMatcher.java src/org/tmatesoft/hg/internal/PathRegexpMatcher.java src/org/tmatesoft/hg/repo/HgIgnore.java src/org/tmatesoft/hg/repo/HgInternals.java src/org/tmatesoft/hg/repo/HgRepository.java
diffstat 12 files changed, 466 insertions(+), 60 deletions(-) [+]
line wrap: on
line diff
--- a/TODO	Wed Feb 02 21:19:02 2011 +0100
+++ b/TODO	Thu Feb 03 22:13:55 2011 +0100
@@ -33,10 +33,13 @@
 * tags
   * Tags are read and can be queried (cmdline Log does)
   
-- keywords
-  - filter with context. filters shall have weight (to allow certain filter come first)
+* keywords
+  + filter with context (HgRepository + Path + Direction (to/from repo)
+  - filters shall have weight (to allow certain filter come first). Would need that once FilterFactories are pluggable
 
-- newlines
+* newlines
+  + \r\n <==> \n
+  - force translation if inconsistent
 
 
 Proposed:
--- a/cmdline/org/tmatesoft/hg/console/Remote.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/cmdline/org/tmatesoft/hg/console/Remote.java	Thu Feb 03 22:13:55 2011 +0100
@@ -33,6 +33,7 @@
 import javax.net.ssl.X509TrustManager;
 
 import org.tmatesoft.hg.internal.ConfigFile;
+import org.tmatesoft.hg.internal.Internals;
 
 /**
  * WORK IN PROGRESS, DO NOT USE
@@ -53,7 +54,7 @@
 	 */
 	public static void main(String[] args) throws Exception {
 		String nid = "d6d2a630f4a6d670c90a5ca909150f2b426ec88f";
-		ConfigFile cfg = new ConfigFile();
+		ConfigFile cfg = new Internals().newConfigFile();
 		cfg.addLocation(new File(System.getProperty("user.home"), ".hgrc"));
 		String svnkitServer = cfg.getSection("paths").get("svnkit");
 		URL url = new URL(svnkitServer + "?cmd=changegroup&roots=a78c980749e3ccebb47138b547e9b644a22797a9");
--- a/src/org/tmatesoft/hg/internal/ConfigFile.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/ConfigFile.java	Thu Feb 03 22:13:55 2011 +0100
@@ -36,18 +36,35 @@
 	private List<String> sections;
 	private List<Map<String,String>> content;
 
-	public ConfigFile() {
+	ConfigFile() {
 	}
 
 	public void addLocation(File path) {
 		read(path);
 	}
 	
+	public boolean hasSection(String sectionName) {
+		return sections == null ? false : sections.indexOf(sectionName) == -1;
+	}
+	
+	// XXX perhaps, should be moved to subclass HgRepoConfig, as it is not common operation for any config file
+	public boolean hasEnabledExtension(String extensionName) {
+		int x = sections != null ? sections.indexOf("extensions") : -1;
+		if (x == -1) {
+			return false;
+		}
+		String value = content.get(x).get(extensionName);
+		return value != null && !"!".equals(value);
+	}
+	
 	public List<String> getSectionNames() {
-		return Collections.unmodifiableList(sections);
+		return sections == null ? Collections.<String>emptyList() : Collections.unmodifiableList(sections);
 	}
 
 	public Map<String,String> getSection(String sectionName) {
+		if (sections ==  null) {
+			return Collections.emptyMap();
+		}
 		int x = sections.indexOf(sectionName);
 		if (x == -1) {
 			return Collections.emptyMap();
@@ -55,7 +72,25 @@
 		return Collections.unmodifiableMap(content.get(x));
 	}
 
+	public boolean getBoolean(String sectionName, String key, boolean defaultValue) {
+		String value = getSection(sectionName).get(key);
+		if (value == null) {
+			return defaultValue;
+		}
+		for (String s : new String[] { "true", "yes", "on", "1" }) {
+			if (s.equalsIgnoreCase(value)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	// TODO handle %include and %unset directives
+	// TODO "" and lists
 	private void read(File f) {
+		if (f == null || !f.canRead()) {
+			return;
+		}
 		if (sections == null) {
 			sections = new ArrayList<String>();
 			content = new ArrayList<Map<String,String>>();
--- a/src/org/tmatesoft/hg/internal/Filter.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/Filter.java	Thu Feb 03 22:13:55 2011 +0100
@@ -31,14 +31,25 @@
 	ByteBuffer filter(ByteBuffer src);
 
 	interface Factory {
-		Filter create(HgRepository hgRepo, Path path, Options opts);
+		void initialize(HgRepository hgRepo, ConfigFile cfg);
+		// may return null if for a given path and/or options this filter doesn't make any sense
+		Filter create(Path path, Options opts);
 	}
 
 	enum Direction {
 		FromRepo, ToRepo
 	}
 
-	abstract class Options {
-		abstract Direction getDirection();
+	public class Options {
+
+		private final Direction direction;
+		public Options(Direction dir) {
+			direction = dir;
+		}
+		
+		Direction getDirection() {
+			return direction;
+		}
+
 	}
 }
--- a/src/org/tmatesoft/hg/internal/Internals.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/Internals.java	Thu Feb 03 22:13:55 2011 +0100
@@ -16,10 +16,12 @@
  */
 package org.tmatesoft.hg.internal;
 
-import static org.tmatesoft.hg.internal.RequiresFile.DOTENCODE;
-import static org.tmatesoft.hg.internal.RequiresFile.FNCACHE;
-import static org.tmatesoft.hg.internal.RequiresFile.STORE;
+import static org.tmatesoft.hg.internal.RequiresFile.*;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.PathRewrite;
 
 /**
@@ -32,6 +34,11 @@
 	
 	private int revlogVersion = 0;
 	private int requiresFlags = 0;
+	private List<Filter.Factory> filterFactories;
+	
+
+	public Internals() {
+	}
 
 	public/*for tests, otherwise pkg*/ void setStorageConfig(int version, int flags) {
 		revlogVersion = version;
@@ -59,4 +66,25 @@
 			};
 		}
 	}
+
+	public ConfigFile newConfigFile() {
+		return new ConfigFile();
+	}
+
+	public List<Filter.Factory> getFilters(HgRepository hgRepo, ConfigFile cfg) {
+		if (filterFactories == null) {
+			filterFactories = new ArrayList<Filter.Factory>();
+			if (cfg.hasEnabledExtension("eol")) {
+				NewlineFilter.Factory ff = new NewlineFilter.Factory();
+				ff.initialize(hgRepo, cfg);
+				filterFactories.add(ff);
+			}
+			if (cfg.hasEnabledExtension("keyword")) {
+				KeywordFilter.Factory ff = new KeywordFilter.Factory();
+				ff.initialize(hgRepo, cfg);
+				filterFactories.add(ff);
+			}
+		}
+		return filterFactories;
+	}
 }
--- a/src/org/tmatesoft/hg/internal/KeywordFilter.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/KeywordFilter.java	Thu Feb 03 22:13:55 2011 +0100
@@ -16,15 +16,13 @@
  */
 package org.tmatesoft.hg.internal;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Map;
 import java.util.TreeMap;
 
-import javax.swing.text.html.Option;
-
 import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.repo.Changeset;
 import org.tmatesoft.hg.repo.HgRepository;
 
 /**
@@ -34,15 +32,22 @@
  */
 public class KeywordFilter implements Filter {
 	// present implementation is stateless, however, filter use pattern shall not assume that. In fact, Factory may us that 
+	private final HgRepository repo;
 	private final boolean isExpanding;
 	private final TreeMap<String,String> keywords;
 	private final int minBufferLen;
+	private final Path path;
+	private Changeset latestFileCset;
 
 	/**
 	 * 
+	 * @param hgRepo 
+	 * @param path 
 	 * @param expand <code>true</code> to expand keywords, <code>false</code> to shrink
 	 */
-	private KeywordFilter(boolean expand) {
+	private KeywordFilter(HgRepository hgRepo, Path p, boolean expand) {
+		repo = hgRepo;
+		path = p;
 		isExpanding = expand;
 		keywords = new TreeMap<String,String>();
 		keywords.put("Id", "Id");
@@ -183,9 +188,13 @@
 		if ("Id".equals(keyword)) {
 			rv.put(identityString().getBytes());
 		} else if ("Revision".equals(keyword)) {
-			rv.put(revision());
+			rv.put(revision().getBytes());
 		} else if ("Author".equals(keyword)) {
 			rv.put(username().getBytes());
+		} else if ("Date".equals(keyword)) {
+			rv.put(date().getBytes());
+		} else {
+			throw new IllegalStateException(String.format("Keyword %s is not yet supported", keyword));
 		}
 	}
 
@@ -202,9 +211,10 @@
 			chars[i] = c;
 		}
 		String kw = new String(chars, 0, i);
-		System.out.println(keywords.subMap("I", "J"));
-		System.out.println(keywords.subMap("A", "B"));
-		System.out.println(keywords.subMap("Au", "B"));
+//		XXX may use subMap to look up keywords based on few available characters (not waiting till closing $)
+//		System.out.println(keywords.subMap("I", "J"));
+//		System.out.println(keywords.subMap("A", "B"));
+//		System.out.println(keywords.subMap("Au", "B"));
 		return keywords.get(kw);
 	}
 	
@@ -235,48 +245,74 @@
 	}
 
 	private String identityString() {
-		return "sample/file.txt, asd";
+		return String.format("%s,v %s %s %s", path, revision(), date(), username());
 	}
 
-	private byte[] revision() {
-		return "1234567890ab".getBytes();
+	private String revision() {
+		// FIXME add cset's nodeid into Changeset class 
+		int csetRev = repo.getFileNode(path).getChangesetLocalRevision(HgRepository.TIP);
+		return repo.getChangelog().getRevision(csetRev).shortNotation();
 	}
 	
 	private String username() {
-		/* ui.py: username()
-        Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
-        and stop searching if one of these is set.
-        If not found and ui.askusername is True, ask the user, else use
-        ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
-        */
-		return "<Sample> sample@sample.org";
+		return getChangeset().user();
+	}
+	
+	private String date() {
+		return String.format("%tY/%<tm/%<td %<tH:%<tM:%<tS", getChangeset().date());
+	}
+	
+	private Changeset getChangeset() {
+		if (latestFileCset == null) {
+			int csetRev = repo.getFileNode(path).getChangesetLocalRevision(HgRepository.TIP);
+			latestFileCset = repo.getChangelog().range(csetRev, csetRev).get(0);
+		}
+		return latestFileCset;
 	}
 
 	public static class Factory implements Filter.Factory {
+		
+		private HgRepository repo;
+		private Path.Matcher matcher;
 
-		public Filter create(HgRepository hgRepo, Path path, Options opts) {
-			return new KeywordFilter(true);
+		public void initialize(HgRepository hgRepo, ConfigFile cfg) {
+			repo = hgRepo;
+			ArrayList<String> patterns = new ArrayList<String>();
+			for (Map.Entry<String,String> e : cfg.getSection("keyword").entrySet()) {
+				if (!"ignore".equalsIgnoreCase(e.getValue())) {
+					patterns.add(e.getKey());
+				}
+			}
+			matcher = new PathGlobMatcher(patterns.toArray(new String[patterns.size()]));
+			// TODO read and respect keyword patterns from [keywordmaps]
+		}
+
+		public Filter create(Path path, Options opts) {
+			if (matcher.accept(path)) {
+				return new KeywordFilter(repo, path, true);
+			}
+			return null;
 		}
 	}
 
-
-	public static void main(String[] args) throws Exception {
-		FileInputStream fis = new FileInputStream(new File("/temp/kwoutput.txt"));
-		FileOutputStream fos = new FileOutputStream(new File("/temp/kwoutput2.txt"));
-		ByteBuffer b = ByteBuffer.allocate(256);
-		KeywordFilter kwFilter = new KeywordFilter(false);
-		while (fis.getChannel().read(b) != -1) {
-			b.flip(); // get ready to be read
-			ByteBuffer f = kwFilter.filter(b);
-			fos.getChannel().write(f); // XXX in fact, f may not be fully consumed
-			if (b.hasRemaining()) {
-				b.compact();
-			} else {
-				b.clear();
-			}
-		}
-		fis.close();
-		fos.flush();
-		fos.close();
-	}
+//
+//	public static void main(String[] args) throws Exception {
+//		FileInputStream fis = new FileInputStream(new File("/temp/kwoutput.txt"));
+//		FileOutputStream fos = new FileOutputStream(new File("/temp/kwoutput2.txt"));
+//		ByteBuffer b = ByteBuffer.allocate(256);
+//		KeywordFilter kwFilter = new KeywordFilter(false);
+//		while (fis.getChannel().read(b) != -1) {
+//			b.flip(); // get ready to be read
+//			ByteBuffer f = kwFilter.filter(b);
+//			fos.getChannel().write(f); // XXX in fact, f may not be fully consumed
+//			if (b.hasRemaining()) {
+//				b.compact();
+//			} else {
+//				b.clear();
+//			}
+//		}
+//		fis.close();
+//		fos.flush();
+//		fos.close();
+//	}
 }
--- a/src/org/tmatesoft/hg/internal/NewlineFilter.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/NewlineFilter.java	Thu Feb 03 22:13:55 2011 +0100
@@ -24,8 +24,11 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Map;
 
 import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.repo.HgInternals;
 import org.tmatesoft.hg.repo.HgRepository;
 
 /**
@@ -154,14 +157,92 @@
 	}
 
 	public static class Factory implements Filter.Factory {
-		private final boolean localIsWin = File.separatorChar == '\\'; // FIXME
-		private final boolean failIfInconsistent = true;
+		private boolean failIfInconsistent = true;
+		private Path.Matcher lfMatcher;
+		private Path.Matcher crlfMatcher;
+		private Path.Matcher binMatcher;
+		private Path.Matcher nativeMatcher;
+		private String nativeRepoFormat;
+		private String nativeOSFormat;
 
-		public Filter create(HgRepository hgRepo, Path path, Options opts) {
-			if (opts.getDirection() == FromRepo) {
-			} else if (opts.getDirection() == ToRepo) {
+		public void initialize(HgRepository hgRepo, ConfigFile cfg) {
+			failIfInconsistent = cfg.getBoolean("eol", "only-consistent", true);
+			File cfgFile = new File(new HgInternals(hgRepo).getRepositoryDir(), ".hgeol");
+			if (!cfgFile.canRead()) {
+				return;
 			}
-			return new NewlineFilter(failIfInconsistent, 1);
+			// XXX if .hgeol is not checked out, we may get it from repository
+//			HgDataFile cfgFileNode = hgRepo.getFileNode(".hgeol");
+//			if (!cfgFileNode.exists()) {
+//				return;
+//			}
+			// XXX perhaps, add HgDataFile.hasWorkingCopy and workingCopyContent()?
+			ConfigFile hgeol = new ConfigFile();
+			hgeol.addLocation(cfgFile);
+			nativeRepoFormat = hgeol.getSection("repository").get("native");
+			if (nativeRepoFormat == null) {
+				nativeRepoFormat = "LF";
+			}
+			final String os = System.getProperty("os.name"); // XXX need centralized set of properties
+			nativeOSFormat = os.indexOf("Windows") != -1 ? "CRLF" : "LF";
+			// I assume pattern ordering in .hgeol is not important
+			ArrayList<String> lfPatterns = new ArrayList<String>();
+			ArrayList<String> crlfPatterns = new ArrayList<String>();
+			ArrayList<String> nativePatterns = new ArrayList<String>();
+			ArrayList<String> binPatterns = new ArrayList<String>();
+			for (Map.Entry<String,String> e : hgeol.getSection("patterns").entrySet()) {
+				if ("CRLF".equals(e.getValue())) {
+					crlfPatterns.add(e.getKey());
+				} else if ("LF".equals(e.getValue())) {
+					lfPatterns.add(e.getKey());
+				} else if ("native".equals(e.getValue())) {
+					nativePatterns.add(e.getKey());
+				} else if ("BIN".equals(e.getValue())) {
+					binPatterns.add(e.getKey());
+				} else {
+					System.out.printf("Can't recognize .hgeol entry: %s for %s", e.getValue(), e.getKey()); // FIXME log warning
+				}
+			}
+			if (!crlfPatterns.isEmpty()) {
+				crlfMatcher = new PathGlobMatcher(crlfPatterns.toArray(new String[crlfPatterns.size()]));
+			}
+			if (!lfPatterns.isEmpty()) {
+				lfMatcher = new PathGlobMatcher(lfPatterns.toArray(new String[lfPatterns.size()]));
+			}
+			if (!binPatterns.isEmpty()) {
+				binMatcher = new PathGlobMatcher(binPatterns.toArray(new String[binPatterns.size()]));
+			}
+			if (!nativePatterns.isEmpty()) {
+				nativeMatcher = new PathGlobMatcher(nativePatterns.toArray(new String[nativePatterns.size()]));
+			}
+		}
+
+		public Filter create(Path path, Options opts) {
+			if (binMatcher == null && crlfMatcher == null && lfMatcher == null && nativeMatcher == null) {
+				// not initialized - perhaps, no .hgeol found
+				return null;
+			}
+			if (binMatcher != null && binMatcher.accept(path)) {
+				return null;
+			}
+			if (crlfMatcher != null && crlfMatcher.accept(path)) {
+				return new NewlineFilter(failIfInconsistent, 1);
+			} else if (lfMatcher != null && lfMatcher.accept(path)) {
+				return new NewlineFilter(failIfInconsistent, 0);
+			} else if (nativeMatcher != null && nativeMatcher.accept(path)) {
+				if (nativeOSFormat.equals(nativeRepoFormat)) {
+					return null;
+				}
+				if (opts.getDirection() == FromRepo) {
+					int transform = "CRLF".equals(nativeOSFormat) ? 1 : 0;
+					return new NewlineFilter(failIfInconsistent, transform);
+				} else if (opts.getDirection() == ToRepo) {
+					int transform = "CRLF".equals(nativeOSFormat) ? 0 : 1;
+					return new NewlineFilter(failIfInconsistent, transform);
+				}
+				return null;
+			}
+			return null;
 		}
 	}
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/PathGlobMatcher.java	Thu Feb 03 22:13:55 2011 +0100
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2011 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@svnkit.com
+ */
+package org.tmatesoft.hg.internal;
+
+import java.util.regex.PatternSyntaxException;
+
+import org.tmatesoft.hg.core.Path;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class PathGlobMatcher implements Path.Matcher {
+	
+	private final PathRegexpMatcher delegate;
+	
+	/**
+	 * 
+	 * @param globPatterns
+	 * @throws NullPointerException if argument is null
+	 * @throws IllegalArgumentException if any of the patterns is not valid
+	 */
+	public PathGlobMatcher(String... globPatterns) {
+		String[] regexp = new String[globPatterns.length]; //deliberately let fail with NPE
+		int i = 0;
+		for (String s : globPatterns) {
+			regexp[i] = glob2regexp(s);
+		}
+		try {
+			delegate = new PathRegexpMatcher(regexp);
+		} catch (PatternSyntaxException ex) {
+			ex.printStackTrace();
+			throw new IllegalArgumentException(ex);
+		}
+	}
+	
+
+	// HgIgnore.glob2regex is similar, but IsIgnore solves slightly different task 
+	// (need to match partial paths, e.g. for glob 'bin' shall match not only 'bin' folder, but also any path below it,
+	// which is not generally the case
+	private static String glob2regexp(String glob) {
+		int end = glob.length() - 1;
+		boolean needLineEndMatch = glob.charAt(end) != '*';
+		while (end > 0 && glob.charAt(end) == '*') end--; // remove trailing * that are useless for Pattern.find()
+		StringBuilder sb = new StringBuilder(end*2);
+		for (int i = 0; i <= end; i++) {
+			char ch = glob.charAt(i);
+			if (ch == '*') {
+				if (glob.charAt(i+1) == '*') { // i < end because we've stripped any trailing * earlier
+					// any char, including path segment separator
+					sb.append(".*?");
+				} else {
+					// just path segments
+					sb.append("[^/]*?");
+				}
+				continue;
+			} else if (ch == '?') {
+				sb.append("[^/]");
+				continue;
+			} else if (ch == '.' || ch == '\\') {
+				sb.append('\\');
+			}
+			sb.append(ch);
+		}
+		if (needLineEndMatch) {
+			sb.append('$');
+		}
+		return sb.toString();
+	}
+
+	public boolean accept(Path path) {
+		return delegate.accept(path);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/PathRegexpMatcher.java	Thu Feb 03 22:13:55 2011 +0100
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2011 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@svnkit.com
+ */
+package org.tmatesoft.hg.internal;
+
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.core.Path.Matcher;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class PathRegexpMatcher implements Matcher {
+	private Pattern[] patterns;
+	
+	// disjunction, matches if any pattern found
+	// uses pattern.find(), not pattern.matches()
+	public PathRegexpMatcher(Pattern... p) {
+		if (p == null) {
+			throw new IllegalArgumentException();
+		}
+		patterns = p;
+	}
+	
+	public PathRegexpMatcher(String... p) throws PatternSyntaxException {
+		this(compile(p));
+	}
+	
+	private static Pattern[] compile(String[] p) throws PatternSyntaxException {
+		// deliberately do no check for null, let it fail
+		Pattern[] rv = new Pattern[p.length];
+		int i = 0;
+		for (String s : p) {
+			rv[i++] = Pattern.compile(s);
+		}
+		return rv;
+	}
+
+	public boolean accept(Path path) {
+		for (Pattern p : patterns) {
+			if (p.matcher(path).find()) {
+				return true;
+			}
+		}
+		return false;
+	}
+}
--- a/src/org/tmatesoft/hg/repo/HgIgnore.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgIgnore.java	Thu Feb 03 22:13:55 2011 +0100
@@ -93,6 +93,7 @@
 		sb.append('^'); // help avoid matcher.find() to match 'bin' pattern in the middle of the filename
 		int start = 0, end = line.length() - 1;
 		// '*' at the beginning and end of a line are useless for Pattern
+		// XXX although how about **.txt - such globs can be seen in a config, are they valid for HgIgnore?
 		while (start <= end && line.charAt(start) == '*') start++;
 		while (end > start && line.charAt(end) == '*') end--;
 
@@ -118,6 +119,7 @@
 		return sb.toString();
 	}
 
+	// TODO use Path and PathGlobMatcher
 	public boolean isIgnored(String path) {
 		for (Pattern p : entries) {
 			if (p.matcher(path).find()) {
--- a/src/org/tmatesoft/hg/repo/HgInternals.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgInternals.java	Thu Feb 03 22:13:55 2011 +0100
@@ -16,6 +16,10 @@
  */
 package org.tmatesoft.hg.repo;
 
+import java.io.File;
+
+import org.tmatesoft.hg.internal.ConfigFile;
+
 
 /**
  * DO NOT USE THIS CLASS, INTENDED FOR TESTING PURPOSES.
@@ -46,4 +50,12 @@
 		}
 		return rv;
 	}
+
+	public File getRepositoryDir() {
+		return repo.getRepositoryRoot();
+	}
+	
+	public ConfigFile getRepoConfig() {
+		return repo.getConfigFile();
+	}
 }
--- a/src/org/tmatesoft/hg/repo/HgRepository.java	Wed Feb 02 21:19:02 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgRepository.java	Thu Feb 03 22:13:55 2011 +0100
@@ -19,10 +19,15 @@
 import java.io.File;
 import java.io.IOException;
 import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 
 import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.internal.ConfigFile;
 import org.tmatesoft.hg.internal.DataAccessProvider;
+import org.tmatesoft.hg.internal.Filter;
 import org.tmatesoft.hg.internal.RequiresFile;
 import org.tmatesoft.hg.internal.RevlogStream;
 import org.tmatesoft.hg.util.FileWalker;
@@ -72,6 +77,7 @@
 	
 	private final org.tmatesoft.hg.internal.Internals impl = new org.tmatesoft.hg.internal.Internals();
 	private HgIgnore ignore;
+	private ConfigFile configFile;
 
 	HgRepository(String repositoryPath) {
 		repoDir = null;
@@ -147,6 +153,7 @@
 		return normalizePath;
 	}
 
+	// local to hide use of io.File. 
 	/*package-local*/ File getRepositoryRoot() {
 		return repoDir;
 	}
@@ -198,8 +205,44 @@
 		}
 		return null; // XXX empty stream instead?
 	}
+	
+	// can't expose internal class, otherwise seems reasonable to have it in API
+	/*package-local*/ ConfigFile getConfigFile() {
+		if (configFile == null) {
+			configFile = impl.newConfigFile();
+			configFile.addLocation(new File(System.getProperty("user.home"), ".hgrc"));
+			// last one, overrides anything else
+			// <repo>/.hg/hgrc
+			configFile.addLocation(new File(getRepositoryRoot(), "hgrc"));
+		}
+		return configFile;
+	}
+	
+	/*package-local*/ List<Filter> getFiltersFromRepoToWorkingDir(Path p) {
+		return instantiateFilters(p, new Filter.Options(Filter.Direction.FromRepo));
+	}
+
+	/*package-local*/ List<Filter> getFiltersFromWorkingDirToRepo(Path p) {
+		return instantiateFilters(p, new Filter.Options(Filter.Direction.ToRepo));
+	}
+
+	private List<Filter> instantiateFilters(Path p, Filter.Options opts) {
+		List<Filter.Factory> factories = impl.getFilters(this, getConfigFile());
+		if (factories.isEmpty()) {
+			return Collections.emptyList();
+		}
+		ArrayList<Filter> rv = new ArrayList<Filter>(factories.size());
+		for (Filter.Factory ff : factories) {
+			Filter f = ff.create(p, opts);
+			if (f != null) {
+				rv.add(f);
+			}
+		}
+		return rv;
+	}
 
 	private void parseRequires() {
 		new RequiresFile().parse(impl, new File(repoDir, "requires"));
 	}
+
 }