changeset 64:19e9e220bf68

Convenient commands constitute hi-level API. org.tmatesoft namespace, GPL2 statement
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 21 Jan 2011 05:56:43 +0100
parents a47530a2ea12
children e21df6259f83
files COPYING TODO design.txt src/com/tmate/hgkit/console/Manifest.java src/com/tmate/hgkit/ll/Changeset.java src/com/tmate/hgkit/ll/HgRepository.java src/com/tmate/hgkit/ll/LocalHgRepo.java src/com/tmate/hgkit/ll/StatusCollector.java src/org/tmatesoft/hg/core/Cset.java src/org/tmatesoft/hg/core/LogCommand.java src/org/tmatesoft/hg/core/Path.java src/org/tmatesoft/hg/core/RepositoryTreeWalker.java src/org/tmatesoft/hg/core/StatusCommand.java src/org/tmatesoft/hg/util/PathPool.java src/org/tmatesoft/hg/util/PathRewrite.java
diffstat 15 files changed, 963 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/COPYING	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,14 @@
+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
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,21 @@
+Read-only support, version 1.0
+==============================
+Committed:
+* hg log
+  user, date, branch, limit
+  filename(multiple?)
+  
+* hg manifest (aka ls)
+  
+* hg status
+
+* hg cat
+
+Proposed:
+- LogCommand.revision(int... rev)+ to walk selected revisions only (list->sort(array) on execute, binary search)
+- LogCommand.before(Date date) and .after()
+- LogCommand.match() to specify pattern, no selected file()s only?  
+
+Read-only support, version 1.1
+==============================
+
--- a/design.txt	Tue Jan 18 18:42:50 2011 +0100
+++ b/design.txt	Fri Jan 21 05:56:43 2011 +0100
@@ -49,6 +49,11 @@
 ??? http://mercurial.selenic.com/wiki/Manifest says "Multiple changesets may refer to the same manifest revision". To me, each changeset 
 changes repository, hence manifest should update nodeids of the files it lists, effectively creating new manifest revision.
 
+? 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)
+
 >>>> Effective file read/data access
 ReadOperation, Revlog does: repo.getFileSystem().run(this.file, new ReadOperation(), long start=0, long end = -1)
 ReadOperation gets buffer (of whatever size, as decided by FS impl), parses it and then  reports if needs more data.
--- a/src/com/tmate/hgkit/console/Manifest.java	Tue Jan 18 18:42:50 2011 +0100
+++ b/src/com/tmate/hgkit/console/Manifest.java	Fri Jan 21 05:56:43 2011 +0100
@@ -5,6 +5,10 @@
 
 import static com.tmate.hgkit.ll.HgRepository.TIP;
 
+import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.core.RepositoryTreeWalker;
+import org.tmatesoft.hg.core.LogCommand.FileRevision;
+
 import com.tmate.hgkit.fs.RepositoryLookup;
 import com.tmate.hgkit.ll.HgManifest;
 import com.tmate.hgkit.ll.HgRepository;
@@ -25,8 +29,26 @@
 			return;
 		}
 		System.out.println(hgRepo.getLocation());
-		HgManifest.Inspector insp = new Dump();
-		hgRepo.getManifest().walk(0, TIP, insp);
+		hgRepo.getManifest().walk(0, TIP, new Dump());
+		//
+		new RepositoryTreeWalker(hgRepo).dirs(true).walk(new RepositoryTreeWalker.Handler() {
+			
+			public void begin(Nodeid manifestRevision) {
+				System.out.println(">> " + manifestRevision);
+			}
+			public void dir(Path p) {
+				System.out.println(p);
+			}
+			public void file(FileRevision fileRevision) {
+				System.out.print(fileRevision.getRevision());;
+				System.out.print("   ");
+				System.out.println(fileRevision.getPath());
+			}
+			
+			public void end(Nodeid manifestRevision) {
+				System.out.println();
+			}
+		}); 
 	}
 
 	public static final class Dump implements HgManifest.Inspector {
--- a/src/com/tmate/hgkit/ll/Changeset.java	Tue Jan 18 18:42:50 2011 +0100
+++ b/src/com/tmate/hgkit/ll/Changeset.java	Fri Jan 21 05:56:43 2011 +0100
@@ -95,6 +95,15 @@
 		sb.append("}");
 		return sb.toString();
 	}
+	
+	@Override
+	public Changeset clone() {
+		try {
+			return (Changeset) super.clone();
+		} catch (CloneNotSupportedException ex) {
+			throw new InternalError(ex.toString());
+		}
+	}
 
 	public static Changeset parse(byte[] data, int offset, int length) {
 		Changeset rv = new Changeset();
--- a/src/com/tmate/hgkit/ll/HgRepository.java	Tue Jan 18 18:42:50 2011 +0100
+++ b/src/com/tmate/hgkit/ll/HgRepository.java	Fri Jan 21 05:56:43 2011 +0100
@@ -3,8 +3,12 @@
  */
 package com.tmate.hgkit.ll;
 
+import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.util.PathRewrite;
+
 
 /**
+ * Shall be as state-less as possible, all the caching happens outside the repo, in commands/walkers
  * @author artem
  */
 public abstract class HgRepository {
@@ -57,8 +61,11 @@
 	protected abstract HgTags createTags();
 
 	public abstract HgDataFile getFileNode(String path);
+	public abstract HgDataFile getFileNode(Path path);
 
 	public abstract String getLocation();
+	
+	public abstract PathRewrite getPathHelper();
 
 
 	protected abstract String toStoragePath(String path, boolean isData);
--- a/src/com/tmate/hgkit/ll/LocalHgRepo.java	Tue Jan 18 18:42:50 2011 +0100
+++ b/src/com/tmate/hgkit/ll/LocalHgRepo.java	Fri Jan 21 05:56:43 2011 +0100
@@ -13,6 +13,9 @@
 import java.util.HashMap;
 import java.util.TreeSet;
 
+import org.tmatesoft.hg.core.Path;
+import org.tmatesoft.hg.util.PathRewrite;
+
 import com.tmate.hgkit.fs.DataAccessProvider;
 import com.tmate.hgkit.fs.FileWalker;
 
@@ -24,6 +27,12 @@
 	private File repoDir; // .hg folder
 	private final String repoLocation;
 	private final DataAccessProvider dataAccess;
+	private final PathRewrite normalizePath = new PathRewrite() {
+		
+		public String rewrite(String path) {
+			return normalize(path);
+		}
+	};
 
 	public LocalHgRepo(String repositoryPath) {
 		setInvalid(true);
@@ -102,6 +111,16 @@
 		return new HgDataFile(this, nPath, content);
 	}
 
+	@Override
+	public HgDataFile getFileNode(Path path) {
+		return getFileNode(path.toString());
+	}
+	
+	@Override
+	public PathRewrite getPathHelper() {
+		return normalizePath;
+	}
+	
 	private boolean revlogv1;
 	private boolean store;
 	private boolean fncache;
--- a/src/com/tmate/hgkit/ll/StatusCollector.java	Tue Jan 18 18:42:50 2011 +0100
+++ b/src/com/tmate/hgkit/ll/StatusCollector.java	Fri Jan 21 05:56:43 2011 +0100
@@ -30,6 +30,10 @@
 		cache.put(-1, emptyFakeState);
 	}
 	
+	public HgRepository getRepo() {
+		return repo;
+	}
+	
 	private ManifestRevisionInspector get(int rev) {
 		ManifestRevisionInspector i = cache.get(rev);
 		if (i == null) {
@@ -58,9 +62,14 @@
 		if (rev1 == rev2) {
 			throw new IllegalArgumentException();
 		}
+		if (inspector == null) {
+			throw new IllegalArgumentException();
+		}
+		if (inspector instanceof Record) {
+			((Record) inspector).init(rev1, rev2, this);
+		}
 		// in fact, rev1 and rev2 are often next (or close) to each other,
 		// thus, we can optimize Manifest reads here (manifest.walk(rev1, rev2))
- 
 		ManifestRevisionInspector r1, r2;
 		if (!cache.containsKey(rev1) && !cache.containsKey(rev2) && Math.abs(rev1 - rev2) < 5 /*subjective equivalent of 'close enough'*/) {
 			int minRev = rev1 < rev2 ? rev1 : rev2;
@@ -119,6 +128,35 @@
 		private List<String> modified, added, removed, clean, missing, unknown, ignored;
 		private Map<String, String> copied;
 		
+		private int startRev, endRev;
+		private StatusCollector statusHelper;
+		
+		// XXX StatusCollector may additionally initialize Record instance to speed lookup of changed file revisions
+		// here I need access to ManifestRevisionInspector via #raw(). Perhaps, non-static class (to get
+		// implicit reference to StatusCollector) may be better?
+		// Since users may want to reuse Record instance we've once created (and initialized), we need to  
+		// ensure functionality is correct for each/any call (#walk checks instanceof Record and fixes it up)
+		// Perhaps, distinct helper (sc.getRevisionHelper().nodeid(fname)) would be better, just not clear
+		// how to supply [start..end] values there easily
+		/*package-local*/void init(int startRevision, int endRevision, StatusCollector self) {
+			startRev = startRevision;
+			endRev = endRevision;
+			statusHelper = self;
+		}
+		
+		public Nodeid nodeidBeforeChange(String fname) {
+			if ((modified == null || !modified.contains(fname)) && (removed == null || !removed.contains(fname))) {
+				return null;
+			}
+			return statusHelper.raw(startRev).nodeid(startRev, fname);
+		}
+		public Nodeid nodeidAfterChange(String fname) {
+			if ((modified == null || !modified.contains(fname)) && (added == null || !added.contains(fname))) {
+				return null;
+			}
+			return statusHelper.raw(endRev).nodeid(endRev, fname);
+		}
+		
 		public List<String> getModified() {
 			return proper(modified);
 		}
@@ -208,6 +246,7 @@
 		}
 	}
 
+	// XXX in fact, indexed access brings more trouble than benefits, get rid of it? Distinct instance per revision is good enough
 	public /*XXX private, actually. Made public unless repo.statusLocal finds better place*/ static final class ManifestRevisionInspector implements HgManifest.Inspector {
 		private final HashMap<String, Nodeid>[] idsMap;
 		private final HashMap<String, String>[] flagsMap;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/Cset.java	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,151 @@
+/*
+ * 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.core;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.tmatesoft.hg.core.LogCommand.FileRevision;
+import org.tmatesoft.hg.util.PathPool;
+
+import com.tmate.hgkit.ll.Changeset;
+import com.tmate.hgkit.ll.HgRepository;
+import com.tmate.hgkit.ll.Nodeid;
+import com.tmate.hgkit.ll.StatusCollector;
+
+/**
+ * TODO rename to Changeset along with original Changeset moved to .repo and renamed to HgChangeset?
+ * Not thread-safe, don't try to read from different threads
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class Cset implements Cloneable {
+	private final StatusCollector statusHelper;
+	private final PathPool pathHelper;
+
+	//
+	private Changeset changeset;
+	private Nodeid nodeid;
+
+	//
+	private List<FileRevision> modifiedFiles, addedFiles;
+	private List<Path> deletedFiles;
+	private int revNumber;
+
+	// XXX consider CommandContext with StatusCollector, PathPool etc. Commands optionally get CC through a cons or create new
+	// and pass it around
+	/*package-local*/Cset(StatusCollector statusCollector, PathPool pathPool) {
+		statusHelper = statusCollector;
+		pathHelper = pathPool;
+	}
+	
+	/*package-local*/
+	void init(int localRevNumber, Nodeid nid, Changeset rawChangeset) {
+		revNumber = localRevNumber;
+		nodeid = nid;
+		changeset = rawChangeset;
+	}
+	
+	public String getUser() {
+		return changeset.user();
+	}
+	public String getComment() {
+		return changeset.comment();
+	}
+	public String getBranch() {
+		return changeset.branch();
+	}
+	public String getDate() {
+		return changeset.dateString();
+	}
+
+	public List<Path> getAffectedFiles() {
+		ArrayList<Path> rv = new ArrayList<Path>(changeset.files().size());
+		for (String name : changeset.files()) {
+			rv.add(pathHelper.path(name));
+		}
+		return rv;
+	}
+
+	public List<FileRevision> getModifiedFiles() {
+		if (modifiedFiles == null) {
+			initFileChanges();
+		}
+		return modifiedFiles;
+	}
+
+	public List<FileRevision> getAddedFiles() {
+		if (addedFiles == null) {
+			initFileChanges();
+		}
+		return addedFiles;
+	}
+
+	public List<Path> getRemovedFiles() {
+		if (deletedFiles == null) {
+			initFileChanges();
+		}
+		return deletedFiles;
+	}
+
+	@Override
+	public Cset clone() {
+		try {
+			Cset copy = (Cset) super.clone();
+			copy.changeset = changeset.clone();
+			return copy;
+		} catch (CloneNotSupportedException ex) {
+			throw new InternalError(ex.toString());
+		}
+	}
+
+	private /*synchronized*/ void initFileChanges() {
+		ArrayList<Path> deleted = new ArrayList<Path>();
+		ArrayList<FileRevision> modified = new ArrayList<FileRevision>();
+		ArrayList<FileRevision> added = new ArrayList<FileRevision>();
+		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);
+			Nodeid nid = r.nodeidAfterChange(s);
+			if (nid == null) {
+				throw new IllegalArgumentException();
+			}
+			modified.add(new FileRevision(repo, nid, p));
+		}
+		for (String s : r.getAdded()) {
+			Path p = pathHelper.path(s);
+			Nodeid nid = r.nodeidAfterChange(s);
+			if (nid == null) {
+				throw new IllegalArgumentException();
+			}
+			added.add(new FileRevision(repo, nid, p));
+		}
+		for (String s : r.getRemoved()) {
+			deleted.add(pathHelper.path(s));
+		}
+		modified.trimToSize();
+		added.trimToSize();
+		deleted.trimToSize();
+		modifiedFiles = Collections.unmodifiableList(modified);
+		addedFiles = Collections.unmodifiableList(added);
+		deletedFiles = Collections.unmodifiableList(deleted);
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/LogCommand.java	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,246 @@
+/*
+ * 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.core;
+
+import static com.tmate.hgkit.ll.HgRepository.TIP;
+
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.tmatesoft.hg.util.PathPool;
+
+import com.tmate.hgkit.ll.Changeset;
+import com.tmate.hgkit.ll.HgRepository;
+import com.tmate.hgkit.ll.Nodeid;
+import com.tmate.hgkit.ll.StatusCollector;
+
+/**
+ * <pre>
+ *   new LogCommand().limit(20).branch("maintenance-2.1").user("me").execute();
+ * </pre>
+ * Not thread-safe (each thread has to use own {@link LogCommand} instance).
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class LogCommand implements Changeset.Inspector {
+
+	private final HgRepository repo;
+	private Set<String> users;
+	private Set<String> branches;
+	private int limit = 0, count = 0;
+	private int startRev = 0, endRev = TIP;
+	private Handler delegate;
+	private Calendar date;
+	private Cset changeset;
+
+	public LogCommand(HgRepository hgRepo) {
+		this.repo = hgRepo;
+	}
+
+	/**
+	 * Limit search to specified user. Multiple user names may be specified.
+	 * @param user - full or partial name of the user, case-insensitive, non-null.
+	 * @return <code>this</code> instance for convenience
+	 */
+	public LogCommand user(String user) {
+		if (user == null) {
+			throw new IllegalArgumentException();
+		}
+		if (users == null) {
+			users = new TreeSet<String>();
+		}
+		users.add(user.toLowerCase());
+		return this;
+	}
+
+	/**
+	 * Limit search to specified branch. Multiple branch specification possible (changeset from any of these 
+	 * would be included in result). If unspecified, all branches are considered.
+	 * @param branch - branch name, case-sensitive, non-null.
+	 * @return <code>this</code> instance for convenience
+	 */
+	public LogCommand branch(String branch) {
+		if (branch == null) {
+			throw new IllegalArgumentException();
+		}
+		if (branches == null) {
+			branches = new TreeSet<String>();
+		}
+		branches.add(branch);
+		return this;
+	}
+	
+	// limit search to specific date
+	// multiple?
+	public LogCommand date(Calendar date) {
+		this.date = date;
+		// FIXME implement
+		// isSet(field) - false => don't use in detection of 'same date'
+		throw HgRepository.notImplemented();
+	}
+	
+	/**
+	 * 
+	 * @param num - number of changeset to produce. Pass 0 to clear the limit. 
+	 * @return <code>this</code> instance for convenience
+	 */
+	public LogCommand limit(int num) {
+		limit = num;
+		return this;
+	}
+
+	/**
+	 * Limit to specified subset of Changelog, [min(rev1,rev2), max(rev1,rev2)], inclusive.
+	 * Revision may be specified with {@link HgRepository#TIP}  
+	 * @param rev1
+	 * @param rev2
+	 * @return <code>this</code> instance for convenience
+	 */
+	public LogCommand range(int rev1, int rev2) {
+		if (rev1 != TIP && rev2 != TIP) {
+			startRev = rev2 < rev1 ? rev2 : rev1;
+			endRev = startRev == rev2 ? rev1 : rev2;
+		} else if (rev1 == TIP && rev2 != TIP) {
+			startRev = rev2;
+			endRev = rev1;
+		} else {
+			startRev = rev1;
+			endRev = rev2;
+		}
+		return this;
+	}
+	
+	// multiple? Bad idea, would need to include extra method into Handler to tell start of next file
+	public LogCommand file(Path file) {
+		// implicit --follow in this case
+		throw HgRepository.notImplemented();
+	}
+
+	/**
+	 * Similar to {@link #execute(com.tmate.hgkit.ll.Changeset.Inspector)}, collects and return result as a list.
+	 */
+	public List<Cset> execute() {
+		CollectHandler collector = new CollectHandler();
+		execute(collector);
+		return collector.getChanges();
+	}
+
+	/**
+	 * 
+	 * @param inspector
+	 * @throws IllegalArgumentException when inspector argument is null
+	 * @throws ConcurrentModificationException if this log command instance is already running
+	 */
+	public void execute(Handler handler) {
+		if (handler == null) {
+			throw new IllegalArgumentException();
+		}
+		if (delegate != null) {
+			throw new ConcurrentModificationException();
+		}
+		try {
+			delegate = handler;
+			count = 0;
+			changeset = new Cset(new StatusCollector(repo), new PathPool(repo.getPathHelper()));
+			repo.getChangelog().range(startRev, endRev, this);
+		} finally {
+			delegate = null;
+			changeset = null;
+		}
+	}
+
+	//
+	
+	public void next(int revisionNumber, Nodeid nodeid, Changeset cset) {
+		if (limit > 0 && count >= limit) {
+			return;
+		}
+		if (branches != null && !branches.contains(cset.branch())) {
+			return;
+		}
+		if (users != null) {
+			String csetUser = cset.user().toLowerCase();
+			boolean found = false;
+			for (String u : users) {
+				if (csetUser.indexOf(u) != -1) {
+					found = true;
+					break;
+				}
+			}
+			if (!found) {
+				return;
+			}
+		}
+		if (date != null) {
+			// FIXME
+		}
+		count++;
+		changeset.init(revisionNumber, nodeid, cset);
+		delegate.next(changeset);
+	}
+
+	public interface Handler {
+		/**
+		 * @param changeset not necessarily a distinct instance each time, {@link Cset#clone() clone()} if need a copy.
+		 */
+		void next(Cset changeset);
+	}
+	
+	public static class CollectHandler implements Handler {
+		private final List<Cset> result = new LinkedList<Cset>();
+
+		public List<Cset> getChanges() {
+			return Collections.unmodifiableList(result);
+		}
+
+		public void next(Cset changeset) {
+			result.add(changeset.clone());
+		}
+	}
+
+	public static final class FileRevision {
+		private final HgRepository repo;
+		private final Nodeid revision;
+		private final Path path;
+		
+		public FileRevision(HgRepository hgRepo, Nodeid rev, Path p) {
+			if (hgRepo == null || rev == null || p == null) {
+				throw new IllegalArgumentException();
+			}
+			repo = hgRepo;
+			revision = rev;
+			path = p;
+		}
+		
+		public Path getPath() {
+			return path;
+		}
+		public Nodeid getRevision() {
+			return revision;
+		}
+		public byte[] getContent() {
+			// XXX Content wrapper, to allow formats other than byte[], e.g. Stream, DataAccess, etc?
+			return repo.getFileNode(path).content();
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/Path.java	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,78 @@
+/*
+ * 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.core;
+
+/**
+ * Identify repository files (not String nor io.File). Convenient for pattern matching. Memory-friendly.
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public final class Path implements CharSequence, Comparable<Path>/*Cloneable? - although clone for paths make no sense*/{
+//	private String[] segments;
+//	private int flags; // dir, unparsed
+	private String path;
+	
+	/*package-local*/Path(String p) {
+		path = p;
+	}
+
+	public int length() {
+		return path.length();
+	}
+
+	public char charAt(int index) {
+		return path.charAt(index);
+	}
+
+	public CharSequence subSequence(int start, int end) {
+		// new Path if start-end matches boundaries of any subpath
+		return path.substring(start, end);
+	}
+	
+	@Override
+	public String toString() {
+		return path; // CharSequence demands toString() impl
+	}
+
+	public int compareTo(Path o) {
+		return path.compareTo(o.path);
+	}
+	
+	@Override
+	public boolean equals(Object obj) {
+		if (obj != null && getClass() == obj.getClass()) {
+			return this == obj || path.equals(((Path) obj).path);
+		}
+		return false;
+	}
+	@Override
+	public int hashCode() {
+		return path.hashCode();
+	}
+
+	public static Path create(String path) {
+		if (path == null) {
+			throw new IllegalArgumentException();
+		}
+		Path rv = new Path(path);
+		return rv;
+	}
+	public interface Matcher {
+		public boolean accept(Path path);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/RepositoryTreeWalker.java	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,162 @@
+/*
+ * 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.core;
+
+import static com.tmate.hgkit.ll.HgRepository.TIP;
+
+import java.util.ConcurrentModificationException;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.tmatesoft.hg.core.LogCommand.FileRevision;
+import org.tmatesoft.hg.util.PathPool;
+
+import com.tmate.hgkit.ll.HgManifest;
+import com.tmate.hgkit.ll.HgRepository;
+import com.tmate.hgkit.ll.Nodeid;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class RepositoryTreeWalker {
+	
+	private final HgRepository repo;
+	private Path.Matcher matcher;
+	private int startRev = 0, endRev = TIP;
+	private Handler visitor;
+	private boolean needDirs = false;
+	
+	private final Mediator mediator = new Mediator();
+
+	public RepositoryTreeWalker(HgRepository hgRepo) {
+		this.repo = hgRepo;
+	}
+
+	public RepositoryTreeWalker range(int rev1, int rev2) {
+		// if manifest range is different from that of changelog, need conversion utils (external?)
+		throw HgRepository.notImplemented();
+	}
+	
+	public RepositoryTreeWalker dirs(boolean include) {
+		// XXX whether directories with directories only are include or not
+		// now lists only directories with files
+		needDirs = include;
+		return this;
+	}
+	
+	/**
+	 * Limit manifest walk to a subset of files. 
+	 * @param pathMatcher - filter, pass <code>null</code> to clear.
+	 * @return <code>this</code> instance for convenience
+	 */
+	public RepositoryTreeWalker match(Path.Matcher pathMatcher) {
+		matcher = pathMatcher;
+		return this;
+	}
+	
+	public void walk(Handler handler) {
+		if (handler == null) {
+			throw new IllegalArgumentException();
+		}
+		if (visitor != null) {
+			throw new ConcurrentModificationException();
+		}
+		try {
+			visitor = handler;
+			mediator.start();
+			repo.getManifest().walk(startRev, endRev, mediator);
+		} finally {
+			visitor = null;
+			mediator.done();
+		}
+	}
+
+	/**
+	 * Callback to walk file/directory tree of a revision
+	 */
+	public interface Handler {
+		void begin(Nodeid manifestRevision);
+		void dir(Path p); // optionally invoked (if walker was configured to spit out directories) prior to any files from this dir and subdirs
+		void file(FileRevision fileRevision); // XXX allow to check p is invalid (df.exists())
+		void end(Nodeid manifestRevision);
+	}
+
+	// I'd rather let RepositoryTreeWalker implement HgManifest.Inspector directly, but this pollutes API alot
+	private class Mediator implements HgManifest.Inspector {
+		private PathPool pathPool;
+		private List<FileRevision> manifestContent;
+		private Nodeid manifestNodeid;
+		
+		public void start() {
+			pathPool = new PathPool(repo.getPathHelper());
+		}
+		
+		public void done() {
+			manifestContent = null;
+			pathPool = null;
+		}
+	
+		public boolean begin(int revision, Nodeid nid) {
+			if (needDirs && manifestContent == null) {
+				manifestContent = new LinkedList<FileRevision>();
+			}
+			visitor.begin(manifestNodeid = nid);
+			return true;
+		}
+		public boolean end(int revision) {
+			if (needDirs) {
+				LinkedHashMap<Path, LinkedList<FileRevision>> breakDown = new LinkedHashMap<Path, LinkedList<FileRevision>>();
+				for (FileRevision fr : manifestContent) {
+					Path filePath = fr.getPath();
+					Path dirPath = pathPool.parent(filePath);
+					LinkedList<FileRevision> revs = breakDown.get(dirPath);
+					if (revs == null) {
+						revs = new LinkedList<FileRevision>();
+						breakDown.put(dirPath, revs);
+					}
+					revs.addLast(fr);
+				}
+				for (Path dir : breakDown.keySet()) {
+					visitor.dir(dir);
+					for (FileRevision fr : breakDown.get(dir)) {
+						visitor.file(fr);
+					}
+				}
+				manifestContent.clear();
+			}
+			visitor.end(manifestNodeid);
+			manifestNodeid = null;
+			return true;
+		}
+		public boolean next(Nodeid nid, String fname, String flags) {
+			Path p = pathPool.path(fname);
+			if (matcher != null && !matcher.accept(p)) {
+				return true;
+			}
+			FileRevision fr = new FileRevision(repo, nid, p);
+			if (needDirs) {
+				manifestContent.add(fr);
+			} else {
+				visitor.file(fr);
+			}
+			return true;
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/StatusCommand.java	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,83 @@
+/*
+ * 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.core;
+
+import org.tmatesoft.hg.core.Path.Matcher;
+
+import com.tmate.hgkit.ll.HgRepository;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class StatusCommand {
+	private final HgRepository repo;
+
+	private boolean needClean = false;
+	private boolean needIgnored = false;
+	private Matcher matcher;
+	private int startRevision;
+	private Integer endRevision; // need three states, set, -1 or actual rev number
+	private boolean visitSubRepo = true;
+
+	public StatusCommand(HgRepository hgRepo) {
+		this.repo = hgRepo;
+	}
+
+	public StatusCommand all() {
+		needClean = true;
+		return this;
+	}
+
+	public StatusCommand clean(boolean include) {
+		needClean = include;
+		return this;
+	}
+	public StatusCommand ignored(boolean include) {
+		needIgnored = include;
+		return this;
+	}
+	
+	// if set, either base:revision or base:workingdir
+	public StatusCommand base(int revision) {
+		startRevision = revision;
+		return this;
+	}
+	
+	// revision without base == --change
+	public StatusCommand revision(int revision) {
+		// XXX how to clear endRevision, if needed.
+		// Perhaps, use of WC_REVISION or BAD_REVISION == -2 or Int.MIN_VALUE?
+		endRevision = new Integer(revision);
+		return this;
+	}
+	
+	public StatusCommand match(Path.Matcher pathMatcher) {
+		matcher = pathMatcher;
+		return this;
+	}
+
+	public StatusCommand subrepo(boolean visit) {
+		visitSubRepo  = visit;
+		throw HgRepository.notImplemented();
+	}
+	
+	public void execute() {
+		throw HgRepository.notImplemented();
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/util/PathPool.java	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,77 @@
+/*
+ * 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.util;
+
+import java.lang.ref.SoftReference;
+import java.util.WeakHashMap;
+
+import org.tmatesoft.hg.core.Path;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class PathPool {
+	private final WeakHashMap<String, SoftReference<Path>> cache;
+	private final PathRewrite pathRewrite;
+	
+	public PathPool(PathRewrite rewrite) {
+		pathRewrite = rewrite;
+		cache = new WeakHashMap<String, SoftReference<Path>>();
+	}
+
+	public Path path(String p) {
+		p = pathRewrite.rewrite(p);
+		SoftReference<Path> sr = cache.get(p);
+		Path path = sr == null ? null : sr.get();
+		if (path == null) {
+			path = Path.create(p);
+			cache.put(p, new SoftReference<Path>(path));
+		}
+		return path;
+	}
+
+	// XXX what would be parent of an empty path?
+	// Path shall have similar functionality
+	public Path parent(Path path) {
+		if (path.length() == 0) {
+			throw new IllegalArgumentException();
+		}
+		for (int i = path.length() - 2 /*if path represents a dir, trailing char is slash, skip*/; i >= 0; i--) {
+			if (path.charAt(i) == '/') {
+				return get(path.subSequence(0, i+1).toString(), true);
+			}
+		}
+		return get("", true);
+	}
+
+	private Path get(String p, boolean create) {
+		SoftReference<Path> sr = cache.get(p);
+		Path path = sr == null ? null : sr.get();
+		if (path == null) {
+			if (create) {
+				path = Path.create(p);
+				cache.put(p, new SoftReference<Path>(path));
+			} else if (sr != null) {
+				// cached path no longer used, clear cache entry - do not wait for RefQueue to step in
+				cache.remove(p);
+			}
+		} 
+		return path;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/util/PathRewrite.java	Fri Jan 21 05:56:43 2011 +0100
@@ -0,0 +1,27 @@
+/*
+ * 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.util;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public interface PathRewrite {
+
+	public String rewrite(String path);
+}