changeset 93:d55d4eedfc57

Switch to Path instead of String in filenames returned by various status operations
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 27 Jan 2011 21:15:21 +0100
parents bf304cb14247
children af1f3b78b918
files cmdline/org/tmatesoft/hg/console/Status.java design.txt src/org/tmatesoft/hg/core/Cset.java src/org/tmatesoft/hg/core/StatusCommand.java src/org/tmatesoft/hg/repo/HgDirstate.java src/org/tmatesoft/hg/repo/HgStatusInspector.java src/org/tmatesoft/hg/repo/StatusCollector.java src/org/tmatesoft/hg/repo/WorkingCopyStatusCollector.java src/org/tmatesoft/hg/util/PathRewrite.java test/org/tmatesoft/hg/test/StatusOutputParser.java test/org/tmatesoft/hg/test/TestStatus.java
diffstat 11 files changed, 361 insertions(+), 205 deletions(-) [+]
line wrap: on
line diff
--- a/cmdline/org/tmatesoft/hg/console/Status.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/cmdline/org/tmatesoft/hg/console/Status.java	Thu Jan 27 21:15:21 2011 +0100
@@ -27,6 +27,7 @@
 import org.tmatesoft.hg.core.Path;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.repo.HgStatusInspector;
 import org.tmatesoft.hg.repo.Internals;
 import org.tmatesoft.hg.repo.StatusCollector;
 import org.tmatesoft.hg.repo.StatusCollector.Record;
@@ -117,13 +118,13 @@
 		sortAndPrint('!', r.getMissing());
 	}
 	
-	private static void sortAndPrint(char prefix, List<String> ul) {
+	private static void sortAndPrint(char prefix, List<Path> ul) {
 		sortAndPrint(prefix, ul, null);
 	}
-	private static void sortAndPrint(char prefix, List<String> ul, Map<String, String> copies) {
-		ArrayList<String> sortList = new ArrayList<String>(ul);
+	private static void sortAndPrint(char prefix, List<Path> ul, Map<Path, Path> copies) {
+		ArrayList<Path> sortList = new ArrayList<Path>(ul);
 		Collections.sort(sortList);
-		for (String s : sortList)  {
+		for (Path s : sortList)  {
 			System.out.print(prefix);
 			System.out.print(' ');
 			System.out.println(s);
@@ -143,52 +144,52 @@
 		}
 	}
 
-	private static class StatusDump implements StatusCollector.Inspector {
+	private static class StatusDump implements HgStatusInspector {
 		public boolean hideStatusPrefix = false; // hg status -n option
 		public boolean showCopied = true; // -C
 		public boolean showIgnored = true; // -i
 		public boolean showClean = true; // -c
 
-		public void modified(String fname) {
+		public void modified(Path fname) {
 			print('M', fname);
 		}
 
-		public void added(String fname) {
+		public void added(Path fname) {
 			print('A', fname);
 		}
 
-		public void copied(String fnameOrigin, String fnameAdded) {
+		public void copied(Path fnameOrigin, Path fnameAdded) {
 			added(fnameAdded);
 			if (showCopied) {
 				print(' ', fnameOrigin);
 			}
 		}
 
-		public void removed(String fname) {
+		public void removed(Path fname) {
 			print('R', fname);
 		}
 
-		public void clean(String fname) {
+		public void clean(Path fname) {
 			if (showClean) {
 				print('C', fname);
 			}
 		}
 
-		public void missing(String fname) {
+		public void missing(Path fname) {
 			print('!', fname);
 		}
 
-		public void unknown(String fname) {
+		public void unknown(Path fname) {
 			print('?', fname);
 		}
 
-		public void ignored(String fname) {
+		public void ignored(Path fname) {
 			if (showIgnored) {
 				print('I', fname);
 			}
 		}
 		
-		private void print(char status, String fname) {
+		private void print(char status, Path fname) {
 			if (!hideStatusPrefix) {
 				System.out.print(status);
 				System.out.print(' ');
--- a/design.txt	Thu Jan 27 06:31:58 2011 +0100
+++ b/design.txt	Thu Jan 27 21:15:21 2011 +0100
@@ -42,6 +42,7 @@
 DataAccess - collect debug info (buffer misses, file size/total read operations) to find out better strategy to buffer size detection. Compare performance.
 
 Strip off metadata from beg of the stream - DataAccess (with rebase/moveBaseOffset(int)) would be handy
+Parameterize StatusCollector to produce copy only when needed. And HgDataFile.metadata perhaps should be moved to cacheable place? 
 
 Status operation from GUI - guess, usually on a file/subfolder, hence API should allow for starting path (unlike cmdline, seems useless to implement include/exclide patterns - GUI users hardly enter them, ever)
   -> recently introduced FileWalker may perhaps help solving this (if starts walking from selected folder) for status op against WorkingDir?
@@ -56,7 +57,9 @@
 ? hg status, compare revision and local file with kw expansion and eol extension
 ? subrepos in log, status (-S) and manifest commands
 
+
 Commands to get CommandContext where they may share various caches (e.g. StatusCollector)
+Perhaps, abstract classes for all Inspectors (i.e. StatusCollector.Inspector) for users to use as base classes to protect from change?
 
 >>>> Effective file read/data access
 ReadOperation, Revlog does: repo.getFileSystem().run(this.file, new ReadOperation(), long start=0, long end = -1)
--- a/src/org/tmatesoft/hg/core/Cset.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/src/org/tmatesoft/hg/core/Cset.java	Thu Jan 27 21:15:21 2011 +0100
@@ -135,24 +135,22 @@
 		StatusCollector.Record r = new StatusCollector.Record();
 		statusHelper.change(revNumber, r);
 		final HgRepository repo = statusHelper.getRepo();
-		for (String s : r.getModified()) {
-			Path p = pathHelper.path(s);
+		for (Path s : r.getModified()) {
 			Nodeid nid = r.nodeidAfterChange(s);
 			if (nid == null) {
 				throw new IllegalArgumentException();
 			}
-			modified.add(new FileRevision(repo, nid, p));
+			modified.add(new FileRevision(repo, nid, s));
 		}
-		for (String s : r.getAdded()) {
-			Path p = pathHelper.path(s);
+		for (Path s : r.getAdded()) {
 			Nodeid nid = r.nodeidAfterChange(s);
 			if (nid == null) {
 				throw new IllegalArgumentException();
 			}
-			added.add(new FileRevision(repo, nid, p));
+			added.add(new FileRevision(repo, nid, s));
 		}
-		for (String s : r.getRemoved()) {
-			deleted.add(pathHelper.path(s));
+		for (Path s : r.getRemoved()) {
+			deleted.add(s);
 		}
 		modified.trimToSize();
 		added.trimToSize();
--- a/src/org/tmatesoft/hg/core/StatusCommand.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/src/org/tmatesoft/hg/core/StatusCommand.java	Thu Jan 27 21:15:21 2011 +0100
@@ -20,8 +20,12 @@
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 import static org.tmatesoft.hg.repo.HgRepository.WORKING_COPY;
 
+import java.util.ConcurrentModificationException;
+
+import org.tmatesoft.hg.core.LogCommand.FileRevision;
 import org.tmatesoft.hg.core.Path.Matcher;
 import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.repo.HgStatusInspector;
 import org.tmatesoft.hg.repo.StatusCollector;
 import org.tmatesoft.hg.repo.WorkingCopyStatusCollector;
 
@@ -33,17 +37,12 @@
 public class StatusCommand {
 	private final HgRepository repo;
 
-	private boolean needModified;
-	private boolean needAdded;
-	private boolean needRemoved;
-	private boolean needUnknown;
-	private boolean needMissing;
-	private boolean needClean;
-	private boolean needIgnored;
-	private Matcher matcher;
 	private int startRevision = TIP;
 	private int endRevision = WORKING_COPY; 
 	private boolean visitSubRepo = true;
+	
+	private HgStatusInspector visitor;
+	private final Mediator mediator = new Mediator();
 
 	public StatusCommand(HgRepository hgRepo) { 
 		repo = hgRepo;
@@ -51,43 +50,45 @@
 	}
 
 	public StatusCommand defaults() {
-		needModified = needAdded = needRemoved = needUnknown = needMissing = true;
-		needClean = needIgnored = false;
+		final Mediator m = mediator;
+		m.needModified = m.needAdded = m.needRemoved = m.needUnknown = m.needMissing = true;
+		m.needClean = m.needIgnored = false;
 		return this;
 	}
 	public StatusCommand all() {
-		needModified = needAdded = needRemoved = needUnknown = needMissing = true;
-		needClean = needIgnored = true;
+		final Mediator m = mediator;
+		m.needModified = m.needAdded = m.needRemoved = m.needUnknown = m.needMissing = true;
+		m.needClean = m.needIgnored = true;
 		return this;
 	}
 	
 
 	public StatusCommand modified(boolean include) {
-		needModified = include;
+		mediator.needModified = include;
 		return this;
 	}
 	public StatusCommand added(boolean include) {
-		needAdded = include;
+		mediator.needAdded = include;
 		return this;
 	}
 	public StatusCommand removed(boolean include) {
-		needRemoved = include;
+		mediator.needRemoved = include;
 		return this;
 	}
 	public StatusCommand deleted(boolean include) {
-		needMissing = include;
+		mediator.needMissing = include;
 		return this;
 	}
 	public StatusCommand unknown(boolean include) {
-		needUnknown = include;
+		mediator.needUnknown = include;
 		return this;
 	}
 	public StatusCommand clean(boolean include) {
-		needClean = include;
+		mediator.needClean = include;
 		return this;
 	}
 	public StatusCommand ignored(boolean include) {
-		needIgnored = include;
+		mediator.needIgnored = include;
 		return this;
 	}
 	
@@ -124,31 +125,130 @@
 		return this;
 	}
 	
+	// pass null to reset
 	public StatusCommand match(Path.Matcher pathMatcher) {
-		matcher = pathMatcher;
-		return this;
+		mediator.matcher = pathMatcher;
+		throw HgRepository.notImplemented();
 	}
 
 	public StatusCommand subrepo(boolean visit) {
 		visitSubRepo  = visit;
 		throw HgRepository.notImplemented();
 	}
-	
-	public void execute(StatusCollector.Inspector inspector) {
+
+	/**
+	 * Perform status operation according to parameters set.
+	 *  
+	 * @param handler callback to get status information
+	 * @throws IllegalArgumentException if handler is <code>null</code>
+	 * @throws ConcurrentModificationException if this command already runs (i.e. being used from another thread)
+	 */
+	public void execute(final HgStatusInspector handler) {
+		if (handler == null) {
+			throw new IllegalArgumentException();
+		}
+		if (visitor != null) {
+			throw new ConcurrentModificationException();
+		}
+		visitor = handler;
 		StatusCollector sc = new StatusCollector(repo); // TODO from CommandContext
-//		StatusCollector.Record r = new StatusCollector.Record(); // XXX use own inspector not to collect entries that
-		// are not interesting or do not match name
-		if (endRevision == WORKING_COPY) {
-			WorkingCopyStatusCollector wcsc = new WorkingCopyStatusCollector(repo);
-			wcsc.setBaseRevisionCollector(sc);
-			wcsc.walk(startRevision, inspector);
-		} else {
-			if (startRevision == TIP) {
-				sc.change(endRevision, inspector);
+//		PathPool pathHelper = new PathPool(repo.getPathHelper()); // TODO from CommandContext
+		try {
+			// XXX if I need a rough estimation (for ProgressMonitor) of number of work units,
+			// I may use number of files in either rev1 or rev2 manifest edition
+			mediator.start();
+			if (endRevision == WORKING_COPY) {
+				WorkingCopyStatusCollector wcsc = new WorkingCopyStatusCollector(repo);
+				wcsc.setBaseRevisionCollector(sc);
+				wcsc.walk(startRevision, mediator);
 			} else {
-				sc.walk(startRevision, endRevision, inspector);
+				if (startRevision == TIP) {
+					sc.change(endRevision, mediator);
+				} else {
+					sc.walk(startRevision, endRevision, mediator);
+				}
+			}
+		} finally {
+			mediator.done();
+			visitor = null;
+		}
+	}
+
+	private class Mediator implements HgStatusInspector {
+		boolean needModified;
+		boolean needAdded;
+		boolean needRemoved;
+		boolean needUnknown;
+		boolean needMissing;
+		boolean needClean;
+		boolean needIgnored;
+		boolean needCopies = false; // FIXME decide if I need such an argument in StatusComment
+		Matcher matcher;
+
+		Mediator() {
+		}
+		
+		public void start() {
+			
+		}
+		public void done() {
+		}
+
+		public void modified(Path fname) {
+			if (needModified) {
+				if (matcher == null || matcher.accept(fname)) {
+					visitor.modified(fname);
+				}
 			}
 		}
-//		PathPool pathHelper = new PathPool(repo.getPathHelper()); // TODO from CommandContext
+		public void added(Path fname) {
+			if (needAdded) {
+				if (matcher == null || matcher.accept(fname)) {
+					visitor.added(fname);
+				}
+			}
+		}
+		public void removed(Path fname) {
+			if (needRemoved) {
+				if (matcher == null || matcher.accept(fname)) {
+					visitor.removed(fname);
+				}
+			}
+		}
+		public void copied(Path fnameOrigin, Path fnameAdded) {
+			if (needCopies) {
+				if (matcher == null || matcher.accept(fnameAdded)) {
+					visitor.copied(fnameOrigin, fnameAdded);
+				}
+			}
+		}
+		public void missing(Path fname) {
+			if (needMissing) {
+				if (matcher == null || matcher.accept(fname)) {
+					visitor.missing(fname);
+				}
+			}
+		}
+		public void unknown(Path fname) {
+			if (needUnknown) {
+				if (matcher == null || matcher.accept(fname)) {
+					visitor.unknown(fname);
+				}
+			}
+		}
+		public void clean(Path fname) {
+			if (needClean) {
+				if (matcher == null || matcher.accept(fname)) {
+					visitor.clean(fname);
+				}
+			}
+		}
+		public void ignored(Path fname) {
+			if (needIgnored) {
+				if (matcher == null || matcher.accept(fname)) {
+					visitor.ignored(fname);
+				}
+			}
+		}
 	}
 }
--- a/src/org/tmatesoft/hg/repo/HgDirstate.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgDirstate.java	Thu Jan 27 21:15:21 2011 +0100
@@ -34,7 +34,7 @@
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
-public class HgDirstate {
+class HgDirstate {
 
 	private final DataAccessProvider accessProvider;
 	private final File dirstateFile;
@@ -143,7 +143,7 @@
 
 
 
-	public void dump() {
+	/*package-local*/ void dump() {
 		read();
 		@SuppressWarnings("unchecked")
 		Map<String, Record>[] all = new Map[] { normal, added, removed, merged };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/repo/HgStatusInspector.java	Thu Jan 27 21:15:21 2011 +0100
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2010-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.repo;
+
+import org.tmatesoft.hg.core.Path;
+
+/**
+ * Callback to get file status information
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public interface HgStatusInspector {
+	void modified(Path fname);
+	void added(Path fname);
+	// XXX need to specify whether StatusCollector invokes added() along with copied or not!
+	void copied(Path fnameOrigin, Path fnameAdded); // if copied files of no interest, should delegate to self.added(fnameAdded);
+	void removed(Path fname);
+	void clean(Path fname);
+	void missing(Path fname); // aka deleted (tracked by Hg, but not available in FS any more
+	void unknown(Path fname); // not tracked
+	void ignored(Path fname);
+}
\ No newline at end of file
--- a/src/org/tmatesoft/hg/repo/StatusCollector.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/StatusCollector.java	Thu Jan 27 21:15:21 2011 +0100
@@ -21,7 +21,6 @@
 
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -31,6 +30,8 @@
 
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.util.PathPool;
+import org.tmatesoft.hg.util.PathRewrite;
 
 
 /**
@@ -43,10 +44,11 @@
 
 	private final HgRepository repo;
 	private final Map<Integer, ManifestRevisionInspector> cache; // sparse array, in fact
+	private PathPool pathPool;
 
 	public StatusCollector(HgRepository hgRepo) {
 		this.repo = hgRepo;
-		cache = new HashMap<Integer, ManifestRevisionInspector>();
+		cache = new TreeMap<Integer, ManifestRevisionInspector>();
 		ManifestRevisionInspector emptyFakeState = new ManifestRevisionInspector();
 		emptyFakeState.begin(-1, null);
 		emptyFakeState.end(-1); // FIXME HgRepo.TIP == -1 as well, need to distinguish fake "prior to first" revision from "the very last" 
@@ -70,9 +72,20 @@
 	/*package-local*/ ManifestRevisionInspector raw(int rev) {
 		return get(rev);
 	}
+	/*package-local*/ PathPool getPathPool() {
+		if (pathPool == null) {
+			pathPool = new PathPool(new PathRewrite.Empty());
+		}
+		return pathPool;
+	}
+
+	public void setPathPool(PathPool pathPool) {
+		this.pathPool = pathPool;
+	}
+		
 	
 	// hg status --change <rev>
-	public void change(int rev, Inspector inspector) {
+	public void change(int rev, HgStatusInspector inspector) {
 		int[] parents = new int[2];
 		repo.getChangelog().parents(rev, parents, null, null);
 		walk(parents[0], rev, inspector);
@@ -81,7 +94,7 @@
 	// I assume revision numbers are the same for changelog and manifest - here 
 	// user would like to pass changelog revision numbers, and I use them directly to walk manifest.
 	// if this assumption is wrong, fix this (lookup manifest revisions from changeset).
-	public void walk(int rev1, int rev2, Inspector inspector) {
+	public void walk(int rev1, int rev2, HgStatusInspector inspector) {
 		if (rev1 == rev2) {
 			throw new IllegalArgumentException();
 		}
@@ -133,7 +146,8 @@
 		r1 = get(rev1);
 		r2 = get(rev2);
 
-		
+		PathPool pp = getPathPool();
+
 		TreeSet<String> r1Files = new TreeSet<String>(r1.files());
 		for (String fname : r2.files()) {
 			if (r1Files.remove(fname)) {
@@ -142,23 +156,22 @@
 				String flagsR1 = r1.flags(fname);
 				String flagsR2 = r2.flags(fname);
 				if (nidR1.equals(nidR2) && ((flagsR2 == null && flagsR1 == null) || flagsR2.equals(flagsR1))) {
-					inspector.clean(fname);
+					inspector.clean(pp.path(fname));
 				} else {
-					inspector.modified(fname);
+					inspector.modified(pp.path(fname));
 				}
 			} else {
 				String copyOrigin = getOriginIfCopy(repo, fname, r1Files, rev1);
 				if (copyOrigin != null) {
-					inspector.copied(copyOrigin, fname);
+					inspector.copied(pp.path(copyOrigin), pp.path(fname));
 				} else {
-					inspector.added(fname);
+					inspector.added(pp.path(fname));
 				}
 			}
 		}
 		for (String left : r1Files) {
-			inspector.removed(left);
+			inspector.removed(pp.path(left));
 		}
-		// inspector.done() if useful e.g. in UI client
 	}
 	
 	public Record status(int rev1, int rev2) {
@@ -188,23 +201,11 @@
 		return null;
 	}
 
-	public interface Inspector {
-		void modified(String fname);
-		void added(String fname);
-		// XXX need to specify whether StatusCollector invokes added() along with copied or not!
-		void copied(String fnameOrigin, String fnameAdded); // if copied files of no interest, should delegate to self.added(fnameAdded);
-		void removed(String fname);
-		void clean(String fname);
-		void missing(String fname); // aka deleted (tracked by Hg, but not available in FS any more
-		void unknown(String fname); // not tracked
-		void ignored(String fname);
-	}
-
 	// XXX for r1..r2 status, only modified, added, removed (and perhaps, clean) make sense
 	// XXX Need to specify whether copy targets are in added or not (@see Inspector#copied above)
-	public static class Record implements Inspector {
-		private List<String> modified, added, removed, clean, missing, unknown, ignored;
-		private Map<String, String> copied;
+	public static class Record implements HgStatusInspector {
+		private List<Path> modified, added, removed, clean, missing, unknown, ignored;
+		private Map<Path, Path> copied;
 		
 		private int startRev, endRev;
 		private StatusCollector statusHelper;
@@ -222,61 +223,61 @@
 			statusHelper = self;
 		}
 		
-		public Nodeid nodeidBeforeChange(String fname) {
+		public Nodeid nodeidBeforeChange(Path fname) {
 			if (statusHelper == null || startRev == BAD_REVISION) {
 				return null;
 			}
 			if ((modified == null || !modified.contains(fname)) && (removed == null || !removed.contains(fname))) {
 				return null;
 			}
-			return statusHelper.raw(startRev).nodeid(fname);
+			return statusHelper.raw(startRev).nodeid(fname.toString());
 		}
-		public Nodeid nodeidAfterChange(String fname) {
+		public Nodeid nodeidAfterChange(Path fname) {
 			if (statusHelper == null || endRev == BAD_REVISION) {
 				return null;
 			}
 			if ((modified == null || !modified.contains(fname)) && (added == null || !added.contains(fname))) {
 				return null;
 			}
-			return statusHelper.raw(endRev).nodeid(fname);
+			return statusHelper.raw(endRev).nodeid(fname.toString());
 		}
 		
-		public List<String> getModified() {
+		public List<Path> getModified() {
 			return proper(modified);
 		}
 
-		public List<String> getAdded() {
+		public List<Path> getAdded() {
 			return proper(added);
 		}
 
-		public List<String> getRemoved() {
+		public List<Path> getRemoved() {
 			return proper(removed);
 		}
 
-		public Map<String,String> getCopied() {
+		public Map<Path,Path> getCopied() {
 			if (copied == null) {
 				return Collections.emptyMap();
 			}
 			return Collections.unmodifiableMap(copied);
 		}
 
-		public List<String> getClean() {
+		public List<Path> getClean() {
 			return proper(clean);
 		}
 
-		public List<String> getMissing() {
+		public List<Path> getMissing() {
 			return proper(missing);
 		}
 
-		public List<String> getUnknown() {
+		public List<Path> getUnknown() {
 			return proper(unknown);
 		}
 
-		public List<String> getIgnored() {
+		public List<Path> getIgnored() {
 			return proper(ignored);
 		}
 		
-		private List<String> proper(List<String> l) {
+		private List<Path> proper(List<Path> l) {
 			if (l == null) {
 				return Collections.emptyList();
 			}
@@ -286,47 +287,47 @@
 		//
 		//
 		
-		public void modified(String fname) {
+		public void modified(Path fname) {
 			modified = doAdd(modified, fname);
 		}
 
-		public void added(String fname) {
+		public void added(Path fname) {
 			added = doAdd(added, fname);
 		}
 
-		public void copied(String fnameOrigin, String fnameAdded) {
+		public void copied(Path fnameOrigin, Path fnameAdded) {
 			if (copied == null) {
-				copied = new LinkedHashMap<String, String>();
+				copied = new LinkedHashMap<Path, Path>();
 			}
 			added(fnameAdded);
 			copied.put(fnameAdded, fnameOrigin);
 		}
 
-		public void removed(String fname) {
+		public void removed(Path fname) {
 			removed = doAdd(removed, fname);
 		}
 
-		public void clean(String fname) {
+		public void clean(Path fname) {
 			clean = doAdd(clean, fname);
 		}
 
-		public void missing(String fname) {
+		public void missing(Path fname) {
 			missing = doAdd(missing, fname);
 		}
 
-		public void unknown(String fname) {
+		public void unknown(Path fname) {
 			unknown = doAdd(unknown, fname);
 		}
 
-		public void ignored(String fname) {
+		public void ignored(Path fname) {
 			ignored = doAdd(ignored, fname);
 		}
 
-		private static List<String> doAdd(List<String> l, String s) {
+		private static List<Path> doAdd(List<Path> l, Path p) {
 			if (l == null) {
-				l = new LinkedList<String>();
+				l = new LinkedList<Path>();
 			}
-			l.add(s);
+			l.add(p);
 			return l;
 		}
 	}
--- a/src/org/tmatesoft/hg/repo/WorkingCopyStatusCollector.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/WorkingCopyStatusCollector.java	Thu Jan 27 21:15:21 2011 +0100
@@ -30,6 +30,8 @@
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.repo.StatusCollector.ManifestRevisionInspector;
 import org.tmatesoft.hg.util.FileWalker;
+import org.tmatesoft.hg.util.PathPool;
+import org.tmatesoft.hg.util.PathRewrite;
 
 /**
  *
@@ -42,6 +44,7 @@
 	private final FileWalker repoWalker;
 	private HgDirstate dirstate;
 	private StatusCollector baseRevisionCollector;
+	private PathPool pathPool;
 
 	public WorkingCopyStatusCollector(HgRepository hgRepo) {
 		this(hgRepo, hgRepo.createWorkingDirWalker());
@@ -59,6 +62,22 @@
 	public void setBaseRevisionCollector(StatusCollector sc) {
 		baseRevisionCollector = sc;
 	}
+
+	/*package-local*/ PathPool getPathPool() {
+		if (pathPool == null) {
+			if (baseRevisionCollector == null) {
+				pathPool = new PathPool(new PathRewrite.Empty());
+			} else {
+				return baseRevisionCollector.getPathPool();
+			}
+		}
+		return pathPool;
+	}
+
+	public void setPathPool(PathPool pathPool) {
+		this.pathPool = pathPool;
+	}
+
 	
 	private HgDirstate getDirstate() {
 		if (dirstate == null) {
@@ -68,7 +87,7 @@
 	}
 
 	// may be invoked few times
-	public void walk(int baseRevision, StatusCollector.Inspector inspector) {
+	public void walk(int baseRevision, HgStatusInspector inspector) {
 		final HgIgnore hgIgnore = repo.getIgnore();
 		TreeSet<String> knownEntries = getDirstate().all();
 		final boolean isTipBase;
@@ -94,12 +113,13 @@
 			((StatusCollector.Record) inspector).init(baseRevision, BAD_REVISION, sc);
 		}
 		repoWalker.reset();
+		final PathPool pp = getPathPool();
 		while (repoWalker.hasNext()) {
 			repoWalker.next();
 			String fname = repoWalker.name();
 			File f = repoWalker.file();
 			if (hgIgnore.isIgnored(fname)) {
-				inspector.ignored(fname);
+				inspector.ignored(pp.path(fname));
 			} else if (knownEntries.remove(fname)) {
 				// modified, added, removed, clean
 				if (collect != null) { // need to check against base revision, not FS file
@@ -109,24 +129,24 @@
 					checkLocalStatusAgainstFile(fname, f, inspector);
 				}
 			} else {
-				inspector.unknown(fname);
+				inspector.unknown(pp.path(fname));
 			}
 		}
 		if (collect != null) {
 			for (String r : baseRevFiles) {
-				inspector.removed(r);
+				inspector.removed(pp.path(r));
 			}
 		}
 		for (String m : knownEntries) {
 			// missing known file from a working dir  
 			if (getDirstate().checkRemoved(m) == null) {
 				// not removed from the repository = 'deleted'  
-				inspector.missing(m);
+				inspector.missing(pp.path(m));
 			} else {
 				// removed from the repo
 				// if we check against non-tip revision, do not report files that were added past that revision and now removed.
 				if (collect == null || baseRevFiles.contains(m)) {
-					inspector.removed(m);
+					inspector.removed(pp.path(m));
 				}
 			}
 		}
@@ -141,31 +161,31 @@
 	//********************************************
 
 	
-	private void checkLocalStatusAgainstFile(String fname, File f, StatusCollector.Inspector inspector) {
+	private void checkLocalStatusAgainstFile(String fname, File f, HgStatusInspector inspector) {
 		HgDirstate.Record r;
 		if ((r = getDirstate().checkNormal(fname)) != null) {
 			// either clean or modified
 			if (f.lastModified() / 1000 == r.time && r.size == f.length()) {
-				inspector.clean(fname);
+				inspector.clean(getPathPool().path(fname));
 			} else {
 				// FIXME check actual content to avoid false modified files
-				inspector.modified(fname);
+				inspector.modified(getPathPool().path(fname));
 			}
 		} else if ((r = getDirstate().checkAdded(fname)) != null) {
 			if (r.name2 == null) {
-				inspector.added(fname);
+				inspector.added(getPathPool().path(fname));
 			} else {
-				inspector.copied(r.name2, fname);
+				inspector.copied(getPathPool().path(r.name2), getPathPool().path(fname));
 			}
 		} else if ((r = getDirstate().checkRemoved(fname)) != null) {
-			inspector.removed(fname);
+			inspector.removed(getPathPool().path(fname));
 		} else if ((r = getDirstate().checkMerged(fname)) != null) {
-			inspector.modified(fname);
+			inspector.modified(getPathPool().path(fname));
 		}
 	}
 	
 	// XXX refactor checkLocalStatus methods in more OO way
-	private void checkLocalStatusAgainstBaseRevision(Set<String> baseRevNames, ManifestRevisionInspector collect, int baseRevision, String fname, File f, StatusCollector.Inspector inspector) {
+	private void checkLocalStatusAgainstBaseRevision(Set<String> baseRevNames, ManifestRevisionInspector collect, int baseRevision, String fname, File f, HgStatusInspector inspector) {
 		// fname is in the dirstate, either Normal, Added, Removed or Merged
 		Nodeid nid1 = collect.nodeid(fname);
 		String flags = collect.flags(fname);
@@ -177,13 +197,13 @@
 			if ((r = getDirstate().checkNormal(fname)) != null) {
 				String origin = StatusCollector.getOriginIfCopy(repo, fname, baseRevNames, baseRevision);
 				if (origin != null) {
-					inspector.copied(origin, fname);
+					inspector.copied(getPathPool().path(origin), getPathPool().path(fname));
 					return;
 				}
 			} else if ((r = getDirstate().checkAdded(fname)) != null) {
 				if (r.name2 != null && baseRevNames.contains(r.name2)) {
 					baseRevNames.remove(r.name2); // XXX surely I shall not report rename source as Removed?
-					inspector.copied(r.name2, fname);
+					inspector.copied(getPathPool().path(r.name2), getPathPool().path(fname));
 					return;
 				}
 				// fall-through, report as added
@@ -191,7 +211,7 @@
 				// removed: removed file was not known at the time of baseRevision, and we should not report it as removed
 				return;
 			}
-			inspector.added(fname);
+			inspector.added(getPathPool().path(fname));
 		} else {
 			// was known; check whether clean or modified
 			// when added - seems to be the case of a file added once again, hence need to check if content is different
@@ -200,14 +220,14 @@
 				HgDataFile fileNode = repo.getFileNode(fname);
 				final int lengthAtRevision = fileNode.length(nid1);
 				if (r.size /* XXX File.length() ?! */ != lengthAtRevision || flags != todoGenerateFlags(fname /*java.io.File*/)) {
-					inspector.modified(fname);
+					inspector.modified(getPathPool().path(fname));
 				} else {
 					// check actual content to see actual changes
 					// XXX consider adding HgDataDile.compare(File/byte[]/whatever) operation to optimize comparison
 					if (areTheSame(f, fileNode.content(nid1))) {
-						inspector.clean(fname);
+						inspector.clean(getPathPool().path(fname));
 					} else {
-						inspector.modified(fname);
+						inspector.modified(getPathPool().path(fname));
 					}
 				}
 			}
--- a/src/org/tmatesoft/hg/util/PathRewrite.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/src/org/tmatesoft/hg/util/PathRewrite.java	Thu Jan 27 21:15:21 2011 +0100
@@ -27,6 +27,12 @@
 public interface PathRewrite {
 
 	public String rewrite(String path);
+	
+	public static class Empty implements PathRewrite {
+		public String rewrite(String path) {
+			return path;
+		}
+	}
 
 	public class Composite implements PathRewrite {
 		private List<PathRewrite> chain;
--- a/test/org/tmatesoft/hg/test/StatusOutputParser.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/test/org/tmatesoft/hg/test/StatusOutputParser.java	Thu Jan 27 21:15:21 2011 +0100
@@ -17,14 +17,17 @@
 package org.tmatesoft.hg.test;
 
 import java.io.File;
-import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.TreeMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.repo.StatusCollector;
+import org.tmatesoft.hg.util.PathPool;
+import org.tmatesoft.hg.util.PathRewrite;
+
 /**
  *
  * @author Artem Tikhomirov
@@ -33,61 +36,70 @@
 public class StatusOutputParser implements OutputParser {
 
 	private final Pattern pattern;
-	private List<String> modified, added, removed, clean, missing, unknown, ignored;
-	private Map<String, String> copied;
-	private final boolean winPathSeparator;
+	// although using StatusCollector.Record is not really quite honest for testing,
+	// it's deemed acceptable as long as that class is primitive 'collect all results'
+	private StatusCollector.Record result = new StatusCollector.Record();
+	private final PathPool pathHelper;
 
 	public StatusOutputParser() {
 //		pattern = Pattern.compile("^([MAR?IC! ]) ([\\w \\.-/\\\\]+)$", Pattern.MULTILINE);
 		pattern = Pattern.compile("^([MAR?IC! ]) (.+)$", Pattern.MULTILINE);
-		winPathSeparator = File.separatorChar == '\\';
+		pathHelper = new PathPool(new PathRewrite() {
+			
+			private final boolean winPathSeparator = File.separatorChar == '\\';
+
+			public String rewrite(String s) {
+				if (winPathSeparator) {
+					// Java impl always give slashed path, while Hg uses local, os-specific convention
+					s = s.replace('\\', '/'); 
+				}
+				return s;
+			}
+		});
 	}
 
 	public void reset() {
-		modified = added = removed = clean = missing = unknown = ignored = null;
-		copied = null;
+		result = new StatusCollector.Record();
 	}
 
 	public void parse(CharSequence seq) {
 		Matcher m = pattern.matcher(seq);
+		Path lastAdded = null;
 		while (m.find()) {
 			String fname = m.group(2);
 			switch ((int) m.group(1).charAt(0)) {
 			case (int) 'M' : {
-				modified = doAdd(modified, fname);
+				result.modified(pathHelper.path(fname));
 				break;
 			}
 			case (int) 'A' : {
-				added = doAdd(added, fname);
+				result.added(lastAdded = pathHelper.path(fname));
 				break;
 			}
 			case (int) 'R' : {
-				removed = doAdd(removed, fname);
+				result.removed(pathHelper.path(fname));
 				break;
 			}
 			case (int) '?' : {
-				unknown = doAdd(unknown, fname);
+				result.unknown(pathHelper.path(fname));
 				break;
 			}
 			case (int) 'I' : {
-				ignored = doAdd(ignored, fname);
+				result.ignored(pathHelper.path(fname));
 				break;
 			}
 			case (int) 'C' : {
-				clean = doAdd(clean, fname);
+				result.clean(pathHelper.path(fname));
 				break;
 			}
 			case (int) '!' : {
-				missing = doAdd(missing, fname);
+				result.missing(pathHelper.path(fname));
 				break;
 			}
 			case (int) ' ' : {
-				if (copied == null) {
-					copied = new TreeMap<String, String>();
-				}
 				// last added is copy destination
 				// to get or to remove it - depends on what StatusCollector does in this case
-				copied.put(added.get(added.size() - 1), toJavaImplConvention(fname));
+				result.copied(pathHelper.path(fname), lastAdded);
 				break;
 			}
 			}
@@ -95,61 +107,39 @@
 	}
 
 	// 
-	public List<String> getModified() {
-		return proper(modified);
-	}
-
-	public List<String> getAdded() {
-		return proper(added);
-	}
-
-	public List<String> getRemoved() {
-		return proper(removed);
-	}
-
-	public Map<String,String> getCopied() {
-		if (copied == null) {
-			return Collections.emptyMap();
-		}
-		return Collections.unmodifiableMap(copied);
-	}
-
-	public List<String> getClean() {
-		return proper(clean);
-	}
-
-	public List<String> getMissing() {
-		return proper(missing);
+	public List<Path> getModified() {
+		return result.getModified();
 	}
 
-	public List<String> getUnknown() {
-		return proper(unknown);
-	}
-
-	public List<String> getIgnored() {
-		return proper(ignored);
-	}
-	
-	private List<String> proper(List<String> l) {
-		if (l == null) {
-			return Collections.emptyList();
+	public List<Path> getAdded() {
+		List<Path> rv = new LinkedList<Path>(result.getAdded());
+		for (Path p : result.getCopied().keySet()) {
+			rv.remove(p); // remove only one duplicate
 		}
-		return Collections.unmodifiableList(l);
+		return rv;
 	}
 
-	private List<String> doAdd(List<String> l, String s) {
-		if (l == null) {
-			l = new LinkedList<String>();
-		}
-		l.add(toJavaImplConvention(s));
-		return l;
+	public List<Path> getRemoved() {
+		return result.getRemoved();
 	}
 
-	private String toJavaImplConvention(String s) {
-		if (winPathSeparator) {
-			// Java impl always give slashed path, while Hg uses local, os-specific convention
-			s = s.replace('\\', '/'); 
-		}
-		return s;
+	public Map<Path,Path> getCopied() {
+		return result.getCopied();
+	}
+
+	public List<Path> getClean() {
+		return result.getClean();
+	}
+
+	public List<Path> getMissing() {
+		return result.getMissing();
+	}
+
+	public List<Path> getUnknown() {
+		return result.getUnknown();
+	}
+
+	public List<Path> getIgnored() {
+		return result.getIgnored();
 	}
 }
--- a/test/org/tmatesoft/hg/test/TestStatus.java	Thu Jan 27 06:31:58 2011 +0100
+++ b/test/org/tmatesoft/hg/test/TestStatus.java	Thu Jan 27 21:15:21 2011 +0100
@@ -22,8 +22,8 @@
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
 
+import org.tmatesoft.hg.core.Path;
 import org.tmatesoft.hg.core.StatusCommand;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.Lookup;
@@ -121,13 +121,13 @@
 		reportNotEqual("IGNORED", r.getIgnored(), statusParser.getIgnored());
 		reportNotEqual("MISSING", r.getMissing(), statusParser.getMissing());
 		reportNotEqual("UNKNOWN", r.getUnknown(), statusParser.getUnknown());
-		List<String> copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet());
-		HashMap<String, String> copyDiff = new HashMap<String,String>();
+		List<Path> copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet());
+		HashMap<Path, String> copyDiff = new HashMap<Path,String>();
 		if (copiedKeyDiff.isEmpty()) {
-			for (String jk : r.getCopied().keySet()) {
-				String jv = r.getCopied().get(jk);
+			for (Path jk : r.getCopied().keySet()) {
+				Path jv = r.getCopied().get(jk);
 				if (statusParser.getCopied().containsKey(jk)) {
-					String cmdv = statusParser.getCopied().get(jk);
+					Path cmdv = statusParser.getCopied().get(jk);
 					if (!jv.equals(cmdv)) {
 						copyDiff.put(jk, jv + " instead of " + cmdv);
 					}
@@ -137,10 +137,10 @@
 			}
 		}
 		System.out.println("COPIED" + (copiedKeyDiff.isEmpty() && copyDiff.isEmpty() ? " are the same" : " are NOT the same:"));
-		for (String s : copiedKeyDiff) {
+		for (Path s : copiedKeyDiff) {
 			System.out.println("\tNon-matching key:" + s);
 		}
-		for (String s : copyDiff.keySet()) {
+		for (Path s : copyDiff.keySet()) {
 			System.out.println(s + " : " + copyDiff.get(s));
 		}
 		// TODO compare equals