changeset 229:1ec6b327a6ac

Scope for status reworked: explicit files or a general matcher
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 31 May 2011 05:23:07 +0200
parents fffe4f882248
children 0dd9da7489dc
files cmdline/org/tmatesoft/hg/console/Main.java src/org/tmatesoft/hg/core/HgStatusCommand.java src/org/tmatesoft/hg/internal/PathGlobMatcher.java src/org/tmatesoft/hg/internal/PathScope.java src/org/tmatesoft/hg/repo/HgInternals.java src/org/tmatesoft/hg/repo/HgRepository.java src/org/tmatesoft/hg/repo/HgStatusCollector.java src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java src/org/tmatesoft/hg/util/FileWalker.java src/org/tmatesoft/hg/util/Path.java test/org/tmatesoft/hg/test/TestStatus.java
diffstat 11 files changed, 418 insertions(+), 89 deletions(-) [+]
line wrap: on
line diff
--- a/cmdline/org/tmatesoft/hg/console/Main.java	Fri May 27 03:01:26 2011 +0200
+++ b/cmdline/org/tmatesoft/hg/console/Main.java	Tue May 31 05:23:07 2011 +0200
@@ -28,6 +28,7 @@
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.internal.ByteArrayChannel;
 import org.tmatesoft.hg.internal.DigestHelper;
+import org.tmatesoft.hg.internal.PathGlobMatcher;
 import org.tmatesoft.hg.repo.HgBranches;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgInternals;
@@ -75,9 +76,26 @@
 	
 	private void testFileStatus() {
 //		final Path path = Path.create("src/org/tmatesoft/hg/util/");
-		final Path path = Path.create("src/org/tmatesoft/hg/internal/Experimental.java");
-		HgWorkingCopyStatusCollector wcsc = HgWorkingCopyStatusCollector.create(hgRepo, path);
+//		final Path path = Path.create("src/org/tmatesoft/hg/internal/Experimental.java");
+//		final Path path = Path.create("dir/file3");
+//		HgWorkingCopyStatusCollector wcsc = HgWorkingCopyStatusCollector.create(hgRepo, path);
+		HgWorkingCopyStatusCollector wcsc = HgWorkingCopyStatusCollector.create(hgRepo, new PathGlobMatcher("*"));
 		wcsc.walk(TIP, new StatusDump());
+		new HgManifestCommand(hgRepo).dirs(true).revision(TIP).execute(new HgManifestCommand.Handler() {
+			
+			public void file(FileRevision fileRevision) {
+			}
+			
+			public void end(Nodeid manifestRevision) {
+			}
+			
+			public void dir(Path p) {
+				System.out.println(p);
+			}
+			
+			public void begin(Nodeid manifestRevision) {
+			}
+		});
 	}
 	
 	private void dumpBranches() {
--- a/src/org/tmatesoft/hg/core/HgStatusCommand.java	Fri May 27 03:01:26 2011 +0200
+++ b/src/org/tmatesoft/hg/core/HgStatusCommand.java	Tue May 31 05:23:07 2011 +0200
@@ -29,7 +29,6 @@
 import org.tmatesoft.hg.repo.HgStatusInspector;
 import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector;
 import org.tmatesoft.hg.util.Path;
-import org.tmatesoft.hg.util.Path.Matcher;
 
 /**
  * Command to obtain file status information, 'hg status' counterpart. 
@@ -41,7 +40,8 @@
 	private final HgRepository repo;
 
 	private int startRevision = TIP;
-	private int endRevision = WORKING_COPY; 
+	private int endRevision = WORKING_COPY;
+	private Path.Matcher scope;
 	
 	private final Mediator mediator = new Mediator();
 
@@ -146,8 +146,8 @@
 	 * @param pathMatcher - matcher to use,  pass <code>null/<code> to reset
 	 * @return <code>this</code> for convenience
 	 */
-	public HgStatusCommand match(Path.Matcher pathMatcher) {
-		mediator.matcher = pathMatcher;
+	public HgStatusCommand match(Path.Matcher scopeMatcher) {
+		scope = scopeMatcher;
 		return this;
 	}
 
@@ -176,10 +176,11 @@
 			// I may use number of files in either rev1 or rev2 manifest edition
 			mediator.start(statusHandler, new ChangelogHelper(repo, startRevision));
 			if (endRevision == WORKING_COPY) {
-				HgWorkingCopyStatusCollector wcsc = new HgWorkingCopyStatusCollector(repo);
+				HgWorkingCopyStatusCollector wcsc = scope != null ? HgWorkingCopyStatusCollector.create(repo, scope) : new HgWorkingCopyStatusCollector(repo);
 				wcsc.setBaseRevisionCollector(sc);
 				wcsc.walk(startRevision, mediator);
 			} else {
+				sc.setScope(scope); // explicitly set, even if null - would be handy once we reuse StatusCollector
 				if (startRevision == TIP) {
 					sc.change(endRevision, mediator);
 				} else {
@@ -204,7 +205,6 @@
 		boolean needClean;
 		boolean needIgnored;
 		boolean needCopies;
-		Matcher matcher;
 		Handler handler;
 		private ChangelogHelper logHelper;
 
@@ -227,59 +227,43 @@
 
 		public void modified(Path fname) {
 			if (needModified) {
-				if (matcher == null || matcher.accept(fname)) {
-					handler.handleStatus(new HgStatus(Modified, fname, logHelper));
-				}
+				handler.handleStatus(new HgStatus(Modified, fname, logHelper));
 			}
 		}
 		public void added(Path fname) {
 			if (needAdded) {
-				if (matcher == null || matcher.accept(fname)) {
-					handler.handleStatus(new HgStatus(Added, fname, logHelper));
-				}
+				handler.handleStatus(new HgStatus(Added, fname, logHelper));
 			}
 		}
 		public void removed(Path fname) {
 			if (needRemoved) {
-				if (matcher == null || matcher.accept(fname)) {
-					handler.handleStatus(new HgStatus(Removed, fname, logHelper));
-				}
+				handler.handleStatus(new HgStatus(Removed, fname, logHelper));
 			}
 		}
 		public void copied(Path fnameOrigin, Path fnameAdded) {
 			if (needCopies) {
-				if (matcher == null || matcher.accept(fnameAdded)) {
-					// FIXME in fact, merged files may report 'copied from' as well, correct status kind thus may differ from Added
-					handler.handleStatus(new HgStatus(Added, fnameAdded, fnameOrigin, logHelper));
-				}
+				// FIXME in fact, merged files may report 'copied from' as well, correct status kind thus may differ from Added
+				handler.handleStatus(new HgStatus(Added, fnameAdded, fnameOrigin, logHelper));
 			}
 		}
 		public void missing(Path fname) {
 			if (needMissing) {
-				if (matcher == null || matcher.accept(fname)) {
-					handler.handleStatus(new HgStatus(Missing, fname, logHelper));
-				}
+				handler.handleStatus(new HgStatus(Missing, fname, logHelper));
 			}
 		}
 		public void unknown(Path fname) {
 			if (needUnknown) {
-				if (matcher == null || matcher.accept(fname)) {
-					handler.handleStatus(new HgStatus(Unknown, fname, logHelper));
-				}
+				handler.handleStatus(new HgStatus(Unknown, fname, logHelper));
 			}
 		}
 		public void clean(Path fname) {
 			if (needClean) {
-				if (matcher == null || matcher.accept(fname)) {
-					handler.handleStatus(new HgStatus(Clean, fname, logHelper));
-				}
+				handler.handleStatus(new HgStatus(Clean, fname, logHelper));
 			}
 		}
 		public void ignored(Path fname) {
 			if (needIgnored) {
-				if (matcher == null || matcher.accept(fname)) {
-					handler.handleStatus(new HgStatus(Ignored, fname, logHelper));
-				}
+				handler.handleStatus(new HgStatus(Ignored, fname, logHelper));
 			}
 		}
 	}
--- a/src/org/tmatesoft/hg/internal/PathGlobMatcher.java	Fri May 27 03:01:26 2011 +0200
+++ b/src/org/tmatesoft/hg/internal/PathGlobMatcher.java	Tue May 31 05:23:07 2011 +0200
@@ -39,7 +39,7 @@
 		String[] regexp = new String[globPatterns.length]; //deliberately let fail with NPE
 		int i = 0;
 		for (String s : globPatterns) {
-			regexp[i] = glob2regexp(s);
+			regexp[i++] = glob2regexp(s);
 		}
 		try {
 			delegate = new PathRegexpMatcher(regexp);
@@ -53,21 +53,28 @@
 	// 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) {
+	private static String glob2regexp(String glob) { // FIXME TESTS NEEDED!!!
 		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()
+		if (glob.length() > 2 && glob.charAt(end) == '*' && glob.charAt(end - 1) == '.') {
+			end-=2;
+		}
+		boolean needLineEndMatch = true;//glob.charAt(end) != '*';
+//		while (end > 0 && glob.charAt(end) == '*') end--; // remove trailing * that are useless for Pattern.find()
 		StringBuilder sb = new StringBuilder(end*2);
-		if (glob.charAt(0) != '*') {
+//		if (glob.charAt(0) != '*') {
 			sb.append('^');
-		}
+//		}
 		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
+				if (i < end && glob.charAt(i+1) == '*') { 
 					// any char, including path segment separator
 					sb.append(".*?");
 					i++;
+					if (i < end && glob.charAt(i+1) == '/') {
+						sb.append("/?");
+						i++;
+					}
 				} else {
 					// just path segments
 					sb.append("[^/]*?");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/PathScope.java	Tue May 31 05:23:07 2011 +0200
@@ -0,0 +1,84 @@
+/*
+ * 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@hg4j.com
+ */
+package org.tmatesoft.hg.internal;
+
+import java.util.ArrayList;
+
+import org.tmatesoft.hg.util.Path;
+
+/**
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class PathScope implements Path.Matcher {
+	private final Path[] files;
+	private final Path[] dirs;
+	private final boolean recursiveDirs;
+
+	public PathScope(boolean recursiveDirs, Path... paths) {
+		if (paths == null) {
+			throw new IllegalArgumentException();
+		}
+		this.recursiveDirs = recursiveDirs;
+		ArrayList<Path> f = new ArrayList<Path>(5);
+		ArrayList<Path> d = new ArrayList<Path>(5);
+		for (Path p : paths) {
+			if (p.isDirectory()) {
+				d.add(p);
+			} else {
+				f.add(p);
+			}
+		}
+		files = f.toArray(new Path[f.size()]);
+		dirs = d.toArray(new Path[d.size()]);
+	}
+
+	public boolean accept(Path path) {
+		if (path.isDirectory()) {
+			// either equals to or parent of a directory we know about. 
+			// If recursiveDirs, accept also if nested to one of our directories.
+			// If one of configured files is nested under the path, accept.
+			for (Path d : dirs) {
+				switch(d.compareWith(path)) {
+				case Same : return true;
+				case Nested : return true;
+				case Parent : return recursiveDirs;
+				}
+			}
+			for (Path f : files) {
+				if (f.compareWith(path) == Path.CompareResult.Nested) {
+					return true;
+				}
+			}
+		} else {
+			for (Path d : dirs) {
+				if (d.compareWith(path) == Path.CompareResult.Parent) {
+					return true;
+				}
+			}
+			for (Path f : files) {
+				if (f.equals(path)) {
+					return true;
+				}
+			}
+			// either lives in a directory in out scope
+			// or there's a file that matches the path
+		}
+		// TODO Auto-generated method stub
+		return false;
+	}
+}
\ No newline at end of file
--- a/src/org/tmatesoft/hg/repo/HgInternals.java	Fri May 27 03:01:26 2011 +0200
+++ b/src/org/tmatesoft/hg/repo/HgInternals.java	Tue May 31 05:23:07 2011 +0200
@@ -23,18 +23,24 @@
 import java.net.UnknownHostException;
 
 import org.tmatesoft.hg.internal.ConfigFile;
+import org.tmatesoft.hg.internal.Experimental;
+import org.tmatesoft.hg.internal.RelativePathRewrite;
+import org.tmatesoft.hg.util.FileIterator;
+import org.tmatesoft.hg.util.FileWalker;
 import org.tmatesoft.hg.util.Path;
+import org.tmatesoft.hg.util.PathRewrite;
 
 
 /**
  * DO NOT USE THIS CLASS, INTENDED FOR TESTING PURPOSES.
  * 
+ * This class gives access to repository internals, and holds methods that I'm not confident have to be widely accessible
  * Debug helper, to access otherwise restricted (package-local) methods
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
-
  */
+@Experimental(reason="Perhaps, shall split methods with debug purpose from methods that are experimental API")
 public class HgInternals {
 
 	private final HgRepository repo;
@@ -86,6 +92,17 @@
 			return username;
 		}
 	}
+	
+	@Experimental(reason="Don't want to expose io.File from HgRepository; need to create FileIterator for working dir. Need a place to keep that code")
+	/*package-local*/ FileIterator createWorkingDirWalker(Path.Matcher workindDirScope) {
+		File repoRoot = repo.getRepositoryRoot().getParentFile();
+		Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(repoRoot), repo.getToRepoPathHelper()));
+		// Impl note: simple source is enough as files in the working dir are all unique
+		// even if they might get reused (i.e. after FileIterator#reset() and walking once again),
+		// path caching is better to be done in the code which knows that path are being reused 
+		return new FileWalker(repoRoot, pathSrc, workindDirScope);
+	}
+
 
 	// Convenient check of local revision number for validity (not all negative values are wrong as long as we use negative constants)
 	public static boolean wrongLocalRevision(int rev) {
--- a/src/org/tmatesoft/hg/repo/HgRepository.java	Fri May 27 03:01:26 2011 +0200
+++ b/src/org/tmatesoft/hg/repo/HgRepository.java	Tue May 31 05:23:07 2011 +0200
@@ -27,11 +27,8 @@
 import org.tmatesoft.hg.internal.ConfigFile;
 import org.tmatesoft.hg.internal.DataAccessProvider;
 import org.tmatesoft.hg.internal.Filter;
-import org.tmatesoft.hg.internal.RelativePathRewrite;
 import org.tmatesoft.hg.internal.RequiresFile;
 import org.tmatesoft.hg.internal.RevlogStream;
-import org.tmatesoft.hg.util.FileIterator;
-import org.tmatesoft.hg.util.FileWalker;
 import org.tmatesoft.hg.util.Path;
 import org.tmatesoft.hg.util.PathRewrite;
 import org.tmatesoft.hg.util.ProgressSupport;
@@ -217,16 +214,6 @@
 		return dataAccess;
 	}
 
-	// FIXME not sure repository shall create walkers
-	/*package-local*/ FileIterator createWorkingDirWalker() {
-		File repoRoot = repoDir.getParentFile();
-		Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(repoRoot), getToRepoPathHelper()));
-		// Impl note: simple source is enough as files in the working dir are all unique
-		// even if they might get reused (i.e. after FileIterator#reset() and walking once again),
-		// path caching is better to be done in the code which knows that path are being reused 
-		return new FileWalker(repoRoot, pathSrc);
-	}
-
 	/**
 	 * Perhaps, should be separate interface, like ContentLookup
 	 * path - repository storage path (i.e. one usually with .i or .d)
--- a/src/org/tmatesoft/hg/repo/HgStatusCollector.java	Fri May 27 03:01:26 2011 +0200
+++ b/src/org/tmatesoft/hg/repo/HgStatusCollector.java	Tue May 31 05:23:07 2011 +0200
@@ -56,7 +56,7 @@
 	private final Pool<Nodeid> cacheNodes;
 	private final Pool<String> cacheFilenames; // XXX in fact, need to think if use of PathPool directly instead is better solution
 	private final ManifestRevisionInspector emptyFakeState;
-	private Path.Matcher scope;
+	private Path.Matcher scope = new Path.Matcher.Any();
 	
 
 	public HgStatusCollector(HgRepository hgRepo) {
@@ -152,7 +152,15 @@
 	public void setPathPool(PathPool pathPool) {
 		this.pathPool = pathPool;
 	}
-		
+
+	/**
+	 * Limit activity of the collector to certain sub-tree of the repository.
+	 * @param scopeMatcher tells whether collector shall report specific path, can be <code>null</code>
+	 */
+	public void setScope(Path.Matcher scopeMatcher) {
+		// do not assign null, ever
+		scope = scopeMatcher == null ? new Path.Matcher.Any() : scopeMatcher;
+	}
 	
 	// hg status --change <rev>
 	public void change(int rev, HgStatusInspector inspector) {
@@ -217,16 +225,7 @@
 		r2 = get(rev2);
 
 		PathPool pp = getPathPool();
-
 		TreeSet<String> r1Files = new TreeSet<String>(r1.files());
-		class MatchAny implements Path.Matcher {
-			public boolean accept(Path path) {
-				return true;
-			}
-		};
-		if (scope == null) {
-			scope = new MatchAny(); // FIXME configure from outside
-		}
 		for (String fname : r2.files()) {
 			final Path r2filePath = pp.path(fname);
 			if (!scope.accept(r2filePath)) {
--- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Fri May 27 03:01:26 2011 +0200
+++ b/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Tue May 31 05:23:07 2011 +0200
@@ -19,14 +19,13 @@
 import static java.lang.Math.max;
 import static java.lang.Math.min;
 import static org.tmatesoft.hg.repo.HgRepository.*;
-import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
-import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.NoSuchElementException;
 import java.util.Set;
@@ -38,7 +37,7 @@
 import org.tmatesoft.hg.internal.ByteArrayChannel;
 import org.tmatesoft.hg.internal.Experimental;
 import org.tmatesoft.hg.internal.FilterByteChannel;
-import org.tmatesoft.hg.internal.RelativePathRewrite;
+import org.tmatesoft.hg.internal.PathScope;
 import org.tmatesoft.hg.repo.HgStatusCollector.ManifestRevisionInspector;
 import org.tmatesoft.hg.util.ByteChannel;
 import org.tmatesoft.hg.util.CancelledException;
@@ -62,10 +61,11 @@
 	private PathPool pathPool;
 
 	public HgWorkingCopyStatusCollector(HgRepository hgRepo) {
-		this(hgRepo, hgRepo.createWorkingDirWalker());
+		this(hgRepo, new HgInternals(hgRepo).createWorkingDirWalker(null));
 	}
 
-	HgWorkingCopyStatusCollector(HgRepository hgRepo, FileIterator hgRepoWalker) {
+	// FIXME document cons
+	public HgWorkingCopyStatusCollector(HgRepository hgRepo, FileIterator hgRepoWalker) {
 		repo = hgRepo;
 		repoWalker = hgRepoWalker;
 	}
@@ -136,7 +136,6 @@
 			repoWalker.next();
 			Path fname = pp.path(repoWalker.name());
 			File f = repoWalker.file();
-			assert f.isFile();
 			if (!f.exists()) {
 				// file coming from iterator doesn't exist.
 				if (knownEntries.remove(fname.toString())) {
@@ -167,6 +166,7 @@
 				}
 				continue;
 			}
+			assert f.isFile();
 			if (knownEntries.remove(fname.toString())) {
 				// tracked file.
 				// modified, added, removed, clean
@@ -399,11 +399,59 @@
 		return null;
 	}
 
-	@Experimental(reason="There's intention to support status query with multiple files/dirs, API might get changed")
-	public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path file) {
-		FileIterator fi = file.isDirectory() ? new DirFileIterator(hgRepo, file) : new FileListIterator(hgRepo.getRepositoryRoot().getParentFile(), file);
+	/**
+	 * Configure status collector to consider only subset of a working copy tree. Tries to be as effective as possible, and to 
+	 * traverse only relevant part of working copy on the filesystem.
+	 * 
+	 * @param hgRepo repository
+	 * @param paths repository-relative files and/or directories. Directories are processed recursively. 
+	 * 
+	 * @return new instance of {@link HgWorkingCopyStatusCollector}, ready to {@link #walk(int, HgStatusInspector) walk} associated working copy 
+	 */
+	@Experimental(reason="Provisional API")
+	public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path... paths) {
+		ArrayList<Path> f = new ArrayList<Path>(5);
+		ArrayList<Path> d = new ArrayList<Path>(5);
+		for (Path p : paths) {
+			if (p.isDirectory()) {
+				d.add(p);
+			} else {
+				f.add(p);
+			}
+		}
+//		final Path[] dirs = f.toArray(new Path[d.size()]);
+		if (d.isEmpty()) {
+			final Path[] files = f.toArray(new Path[f.size()]);
+			FileIterator fi = new FileListIterator(hgRepo.getRepositoryRoot().getParentFile(), files);
+			return new HgWorkingCopyStatusCollector(hgRepo, fi);
+		}
+		//
+		
+		//FileIterator fi = file.isDirectory() ? new DirFileIterator(hgRepo, file) : new FileListIterator(, file);
+		FileIterator fi = new HgInternals(hgRepo).createWorkingDirWalker(new PathScope(true, paths));
 		return new HgWorkingCopyStatusCollector(hgRepo, fi);
 	}
+	
+	/**
+	 * Configure collector object to calculate status for matching files only. 
+	 * This method may be less effective than explicit list of files as it iterates over whole repository 
+	 * (thus supplied matcher doesn't need to care if directories to files in question are also in scope, 
+	 * see {@link FileWalker#FileWalker(File, Path.Source, Path.Matcher)})
+	 *  
+	 * @return new instance of {@link HgWorkingCopyStatusCollector}, ready to {@link #walk(int, HgStatusInspector) walk} associated working copy
+	 */
+	@Experimental(reason="Provisional API. May add boolean strict argument for those who write smart matchers that can be used in FileWalker")
+	public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path.Matcher scope) {
+		FileIterator w = new HgInternals(hgRepo).createWorkingDirWalker(null);
+		FileIterator wf = (scope == null || scope instanceof Path.Matcher.Any) ? w : new FileIteratorFilter(w, scope);
+		// the reason I need to iterate over full repo and apply filter is that I have no idea whatsoever about
+		// patterns in the scope. I.e. if scope lists a file (PathGlobMatcher("a/b/c.txt")), FileWalker won't get deep
+		// to the file unless matcher would also explicitly include "a/", "a/b/" in scope. Since I can't rely
+		// users would write robust matchers, and I don't see a decent way to enforce that (i.e. factory to produce
+		// correct matcher from Path is much like what PathScope does, and can be accessed directly with #create(repo, Path...)
+		// method above/
+		return new HgWorkingCopyStatusCollector(hgRepo, wf);
+	}
 
 	private static class FileListIterator implements FileIterator {
 		private final File dir;
@@ -452,15 +500,16 @@
 		}
 	}
 	
-	private static class DirFileIterator implements FileIterator {
-		private final Path dirOfInterest;
-		private final FileWalker walker;
+	private static class FileIteratorFilter implements FileIterator {
+		private final Path.Matcher filter;
+		private final FileIterator walker;
+		private boolean didNext = false;
 
-		public DirFileIterator(HgRepository hgRepo, Path directory) {
-			dirOfInterest = directory;
-			File dir = hgRepo.getRepositoryRoot().getParentFile();
-			Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(dir), hgRepo.getToRepoPathHelper()));
-			walker = new FileWalker(new File(dir, directory.toString()), pathSrc);
+		public FileIteratorFilter(FileIterator fileWalker, Path.Matcher filterMatcher) {
+			assert fileWalker != null;
+			assert filterMatcher != null;
+			filter = filterMatcher;
+			walker = fileWalker;
 		}
 
 		public void reset() {
@@ -468,11 +517,24 @@
 		}
 
 		public boolean hasNext() {
-			return walker.hasNext();
+			while (walker.hasNext()) {
+				walker.next();
+				if (filter.accept(walker.name())) {
+					didNext = true;
+					return true;
+				}
+			}
+			return false;
 		}
 
 		public void next() {
-			walker.next();
+			if (didNext) {
+				didNext = false;
+			} else {
+				if (!hasNext()) {
+					throw new NoSuchElementException();
+				}
+			}
 		}
 
 		public Path name() {
@@ -484,7 +546,7 @@
 		}
 
 		public boolean inScope(Path file) {
-			return file.toString().startsWith(dirOfInterest.toString());
+			return filter.accept(file);
 		}
 	}
 }
--- a/src/org/tmatesoft/hg/util/FileWalker.java	Fri May 27 03:01:26 2011 +0200
+++ b/src/org/tmatesoft/hg/util/FileWalker.java	Tue May 31 05:23:07 2011 +0200
@@ -31,14 +31,28 @@
 	private final Path.Source pathHelper;
 	private final LinkedList<File> dirQueue;
 	private final LinkedList<File> fileQueue;
+	private final Path.Matcher scope;
 	private File nextFile;
 	private Path nextPath;
 
 	public FileWalker(File dir, Path.Source pathFactory) {
+		this(dir, pathFactory, null);
+	}
+
+	/**
+	 * 
+	 * @param dir
+	 * @param pathFactory
+	 * @param scopeMatcher - this matcher shall be capable to tell not only files of interest, but
+	 * also whether directories shall be traversed or not (Paths it gets in {@link Path.Matcher#accept(Path)} may 
+	 * point to directories)   
+	 */
+	public FileWalker(File dir, Path.Source pathFactory, Path.Matcher scopeMatcher) {
 		startDir = dir;
 		pathHelper = pathFactory;
 		dirQueue = new LinkedList<File>();
 		fileQueue = new LinkedList<File>();
+		scope = scopeMatcher;
 		reset();
 	}
 
@@ -71,7 +85,8 @@
 	}
 	
 	public boolean inScope(Path file) {
-		return true; // no limits, all files are of interest
+		/* by default, no limits, all files are of interest */
+		return scope == null ? true : scope.accept(file); 
 	}
 	
 	// returns non-null
@@ -91,8 +106,13 @@
 			while (!dirQueue.isEmpty()) {
 				File dir = dirQueue.removeFirst();
 				for (File f : listFiles(dir)) {
-					if (f.isDirectory()) {
-						if (!".hg".equals(f.getName())) {
+					final boolean isDir = f.isDirectory();
+					Path path = pathHelper.path(isDir ? ensureTrailingSlash(f.getPath()) : f.getPath());
+					if (!inScope(path)) {
+						continue;
+					}
+					if (isDir) {
+						if (!".hg/".equals(path.toString())) {
 							dirQueue.addLast(f);
 						}
 					} else {
@@ -104,4 +124,17 @@
 		}
 		return !fileQueue.isEmpty();
 	}
+	
+	private static String ensureTrailingSlash(String dirName) {
+		if (dirName.length() > 0) {
+			char last = dirName.charAt(dirName.length() - 1);
+			if (last == '/' || last == File.separatorChar) {
+				return dirName;
+			}
+			// if path already has platform-specific separator (which, BTW, it shall, according to File#getPath), 
+			// add similar, otherwise use our default.
+			return dirName.indexOf(File.separatorChar) != -1 ? dirName.concat(File.separator) : dirName.concat("/");
+		}
+		return dirName;
+	}
 }
--- a/src/org/tmatesoft/hg/util/Path.java	Fri May 27 03:01:26 2011 +0200
+++ b/src/org/tmatesoft/hg/util/Path.java	Tue May 31 05:23:07 2011 +0200
@@ -16,6 +16,8 @@
  */
 package org.tmatesoft.hg.util;
 
+import java.util.Collection;
+
 /**
  * Identify repository files (not String nor io.File). Convenient for pattern matching. Memory-friendly.
  * 
@@ -75,6 +77,29 @@
 	public int hashCode() {
 		return path.hashCode();
 	}
+	
+	public enum CompareResult {
+		Same, Unrelated, Nested, Parent, /* perhaps, also ImmediateParent, DirectChild? */
+	}
+	
+	/*
+	 * a/file and a/dir ?
+	 */
+	public CompareResult compareWith(Path another) {
+		if (another == null) {
+			return CompareResult.Unrelated; // XXX perhaps, IAE?
+		}
+		if (another == this || (another.length() == length() && equals(another))) {
+			return CompareResult.Same;
+		}
+		if (path.startsWith(another.path)) {
+			return CompareResult.Nested;
+		}
+		if (another.path.startsWith(path)) {
+			return CompareResult.Parent;
+		}
+		return CompareResult.Unrelated;
+	}
 
 	public static Path create(String path) {
 		if (path == null) {
@@ -92,6 +117,26 @@
 	 */
 	public interface Matcher {
 		boolean accept(Path path);
+		
+		final class Any implements Matcher {
+			public boolean accept(Path path) { return true; }
+		}
+		class Composite implements Matcher {
+			private final Path.Matcher[] elements;
+			
+			public Composite(Collection<Path.Matcher> matchers) {
+				elements = matchers.toArray(new Path.Matcher[matchers.size()]);
+			}
+
+			public boolean accept(Path path) {
+				for (Path.Matcher m : elements) {
+					if (m.accept(path)) {
+						return true;
+					}
+				}
+				return false;
+			}
+		}
 	}
 
 	/**
--- a/test/org/tmatesoft/hg/test/TestStatus.java	Fri May 27 03:01:26 2011 +0200
+++ b/test/org/tmatesoft/hg/test/TestStatus.java	Tue May 31 05:23:07 2011 +0200
@@ -16,8 +16,9 @@
  */
 package org.tmatesoft.hg.test;
 
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.hamcrest.CoreMatchers.equalTo;
 import static org.tmatesoft.hg.core.HgStatus.Kind.*;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
@@ -35,6 +36,7 @@
 import org.tmatesoft.hg.core.HgStatus;
 import org.tmatesoft.hg.core.HgStatus.Kind;
 import org.tmatesoft.hg.core.HgStatusCommand;
+import org.tmatesoft.hg.internal.PathGlobMatcher;
 import org.tmatesoft.hg.repo.HgLookup;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgStatusCollector;
@@ -268,6 +270,97 @@
 		assertTrue(sc.get(file1).size() == 1);
 	}
 	
+	@Test
+	public void testSubTreeStatus() throws Exception {
+		repo = Configuration.get().find("status-1");
+		HgStatusCommand cmd = new HgStatusCommand(repo);
+		StatusCollector sc = new StatusCollector();
+		cmd.match(new PathGlobMatcher("*"));
+		cmd.all().execute(sc);
+		/*
+		 * C .hgignore
+		 * ? file1
+		 * M file2
+		 * C readme
+		 */
+		final Path file1 = Path.create("file1");
+		assertTrue(sc.get(file1).contains(Unknown));
+		assertTrue(sc.get(file1).size() == 1);
+		assertTrue(sc.get(Removed).isEmpty());
+		assertTrue(sc.get(Clean).size() == 2);
+		assertTrue(sc.get(Modified).size() == 1);
+		//
+		cmd.match(new PathGlobMatcher("dir/*")).execute(sc = new StatusCollector());
+		/*
+		 * I dir/file3
+		 * R dir/file4
+		 * R dir/file5
+		 */
+		assertTrue(sc.get(Modified).isEmpty());
+		assertTrue(sc.get(Added).isEmpty());
+		assertTrue(sc.get(Ignored).size() == 1);
+		assertTrue(sc.get(Removed).size() == 2);
+	}
+	
+	
+	@Test
+	public void testSpecificFileStatus() throws Exception {
+		repo = Configuration.get().find("status-1");
+		// files only
+		final Path file2 = Path.create("file2");
+		final Path file3 = Path.create("dir/file3");
+		HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file2, file3);
+		HgStatusCollector.Record r = new HgStatusCollector.Record();
+		sc.walk(TIP, r);
+		assertTrue(r.getAdded().isEmpty());
+		assertTrue(r.getRemoved().isEmpty());
+		assertTrue(r.getUnknown().isEmpty());
+		assertTrue(r.getClean().isEmpty());
+		assertTrue(r.getMissing().isEmpty());
+		assertTrue(r.getCopied().isEmpty());
+		assertTrue(r.getIgnored().contains(file3));
+		assertTrue(r.getIgnored().size() == 1);
+		assertTrue(r.getModified().contains(file2));
+		assertTrue(r.getModified().size() == 1);
+		// mix files and directories
+		final Path readme = Path.create("readme");
+		final Path dir = Path.create("dir/");
+		sc = HgWorkingCopyStatusCollector.create(repo, readme, dir);
+		sc.walk(TIP, r = new HgStatusCollector.Record());
+		assertTrue(r.getAdded().isEmpty());
+		assertTrue(r.getRemoved().size() == 2);
+		for (Path p : r.getRemoved()) {
+			assertEquals(p.compareWith(dir), Path.CompareResult.Nested);
+		}
+		assertTrue(r.getUnknown().isEmpty());
+		assertTrue(r.getClean().size() == 1);
+		assertTrue(r.getClean().contains(readme));
+		assertTrue(r.getMissing().isEmpty());
+		assertTrue(r.getCopied().isEmpty());
+		assertTrue(r.getIgnored().contains(file3));
+		assertTrue(r.getIgnored().size() == 1);
+		assertTrue(r.getModified().isEmpty());
+	}
+	
+	@Test
+	public void testSameResultDirectPathVsMatcher() throws Exception {
+		repo = Configuration.get().find("status-1");
+		final Path file3 = Path.create("dir/file3");
+		final Path file5 = Path.create("dir/file5");
+		
+		HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file3, file5);
+		HgStatusCollector.Record r;
+		sc.walk(TIP, r = new HgStatusCollector.Record());
+		assertTrue(r.getRemoved().contains(file5));
+		assertTrue(r.getIgnored().contains(file3));
+		//
+		// query for the same file, but with
+		sc = HgWorkingCopyStatusCollector.create(repo, new PathGlobMatcher(file3.toString(), file5.toString()));
+		sc.walk(TIP, r = new HgStatusCollector.Record());
+		assertTrue(r.getRemoved().contains(file5));
+		assertTrue(r.getIgnored().contains(file3));
+	}
+	
 	/*
 	 * With warm-up of previous tests, 10 runs, time in milliseconds
 	 * 'hg status -A': Native client total 953 (95 per run), Java client 94 (9)