changeset 414:bb278ccf9866

Pull changes from smartgit3 branch
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 21 Mar 2012 20:51:12 +0100
parents 7f27122011c3 (diff) 63c5a9d7ca3f (current diff)
children ee8264d80747
files src/org/tmatesoft/hg/internal/DataAccessProvider.java src/org/tmatesoft/hg/internal/Internals.java src/org/tmatesoft/hg/repo/HgInternals.java src/org/tmatesoft/hg/repo/HgManifest.java src/org/tmatesoft/hg/repo/HgRemoteRepository.java src/org/tmatesoft/hg/repo/HgRepository.java
diffstat 42 files changed, 1262 insertions(+), 436 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Wed Mar 21 14:54:02 2012 +0100
+++ b/.hgtags	Wed Mar 21 20:51:12 2012 +0100
@@ -1,2 +1,3 @@
 c2601c0b4a1fd5940054eec95b92ea719e66cb78 v0.5.0
 fc8bc2f1edbe876c66c8fcbfc054bb836c733b06 v0.7.0
+f52ca9530774436ce5a9192e7c5a825a5018b65d v0.8.0
--- a/build.xml	Wed Mar 21 14:54:02 2012 +0100
+++ b/build.xml	Wed Mar 21 20:51:12 2012 +0100
@@ -27,7 +27,7 @@
 
 	<property name="junit.jar" value="lib/junit-4.8.2.jar" />
 	<property name="ver.qualifier" value="" />
-	<property name="version.lib" value="0.8.0" />
+	<property name="version.lib" value="0.9.0" />
 	<property name="version.jar" value="${version.lib}${ver.qualifier}" />
 	<property name="compile-with-debug" value="yes"/>
 
@@ -145,6 +145,7 @@
 		<jar destfile="${hg4j-tests.jar}">
 			<fileset dir="bin" includes="org/tmatesoft/hg/test/**"/>
 			<fileset file="COPYING"/>
+			<!-- XXX perhaps, shall include test-data as well? -->
 		</jar>
 	</target>
 
--- a/design.txt	Wed Mar 21 14:54:02 2012 +0100
+++ b/design.txt	Wed Mar 21 20:51:12 2012 +0100
@@ -44,6 +44,7 @@
 
 delta merge
 DataAccess - collect debug info (buffer misses, file size/total read operations) to find out better strategy to buffer size detection. Compare performance.
+RevlogStream - inflater buffer (and other buffers) size may be too small for repositories out there (i.e. inflater buffer of 512 bytes for 200k revision)  
 
 
 Parameterize StatusCollector to produce copy only when needed. And HgDataFile.metadata perhaps should be moved to cacheable place?
--- a/src/org/tmatesoft/hg/core/ChangesetTransformer.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/ChangesetTransformer.java	Wed Mar 21 20:51:12 2012 +0100
@@ -108,7 +108,8 @@
 			changeset = new HgChangeset(statusCollector, pp);
 			changeset.setParentHelper(pw);
 		}
-		
+
+		// FIXME document instance reuse policy
 		HgChangeset handle(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
 			changeset.init(revisionNumber, nodeid, cset);
 			return changeset;
--- a/src/org/tmatesoft/hg/core/HgBadStateException.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgBadStateException.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,6 +18,7 @@
 
 /**
  * hg4j's own internal error or unexpected state.
+ * XXX unless there's anything additional, there's not too much value in this class
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
@@ -25,11 +26,6 @@
 @SuppressWarnings("serial")
 public class HgBadStateException extends RuntimeException {
 
-	// FIXME quick-n-dirty fix, don't allow exceptions without a cause
-	public HgBadStateException() {
-		super("Internal error");
-	}
-
 	public HgBadStateException(String message) {
 		super(message);
 	}
--- a/src/org/tmatesoft/hg/core/HgCallbackTargetException.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgCallbackTargetException.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -29,6 +29,7 @@
  * {@link RuntimeException} subclass, {@link Wrap}. Then, unwrap and re-throw with checked {@link HgCallbackTargetException}. 
  * 
  * FIXME REVISIT perhaps, shall just throw HgCallbackTargetException from any handler, and do not catch anything in commands at all?
+ * FIXME decide whether shall root at HgException ("throws HgException, HgCallbackTargetException" looks a bit odd now) 
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
@@ -66,7 +67,7 @@
 		sb.append("Original exception thrown: ");
 		sb.append(getCause().getClass().getName());
 		sb.append(" at ");
-		appendDetails(sb);
+		extras.appendDetails(sb);
 		return sb.toString();
 	}
 
--- a/src/org/tmatesoft/hg/core/HgCatCommand.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgCatCommand.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
 import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
@@ -132,10 +131,17 @@
 	 * Runs the command with current set of parameters and pipes data to provided sink.
 	 * 
 	 * @param sink output channel to write data to.
-	 * @throws HgDataStreamException 
+	 * 
+	 * @throws HgBadArgumentException if no target file node found 
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgInvalidFileException if access to file in working directory failed
+	 * @throws HgException in case of some other library issue 
+	 * @throws CancelledException if execution of the operation was cancelled
+	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog (<em>runtime exception</em>)
 	 * @throws IllegalArgumentException when command arguments are incomplete or wrong
 	 */
-	public void execute(ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException {
+	public void execute(ByteChannel sink) throws HgException, CancelledException {
+		// XXX perhaps, IAE together with HgBadArgumentException is not the best idea
 		if (revisionIndex == BAD_REVISION && revision == null && cset == null) {
 			throw new IllegalArgumentException("File revision, corresponing local number, or a changset nodeid shall be specified");
 		}
@@ -147,7 +153,8 @@
 		}
 		HgDataFile dataFile = repo.getFileNode(file);
 		if (!dataFile.exists()) {
-			throw new HgDataStreamException(file, new FileNotFoundException(file.toString()));
+			// TODO may benefit from repo.getStoragePath to print revlog location in addition to human-friendly file path 
+			throw new HgBadArgumentException(String.format("File %s not found in the repository", file), null).setFileName(file);
 		}
 		int revToExtract;
 		if (cset != null) {
--- a/src/org/tmatesoft/hg/core/HgChangeset.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgChangeset.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -78,18 +78,52 @@
 		}
 	}
 
+	/**
+	 * Index of the changeset in local repository. Note, this number is relevant only for local repositories/operations, use 
+	 * {@link Nodeid nodeid} to uniquely identify a revision.
+	 *   
+	 * @return index of the changeset revision
+	 */
+	public int getRevisionIndex() {
+		return revNumber;
+	}
+
+	/**
+	 * @deprecated use {@link #getRevisionIndex()}
+	 */
+	@Deprecated
 	public int getRevision() {
 		return revNumber;
 	}
+
+	/**
+	 * Unique identity of this changeset revision
+	 * @return revision identifier, never <code>null</code>
+	 */
 	public Nodeid getNodeid() {
 		return nodeid;
 	}
+
+	/**
+	 * Name of the user who made this commit
+	 * @return author of the commit, never <code>null</code>
+	 */
 	public String getUser() {
 		return changeset.user();
 	}
+	
+	/**
+	 * Commit description
+	 * @return content of the corresponding field in changeset record; empty string if none specified.
+	 */
 	public String getComment() {
 		return changeset.comment();
 	}
+
+	/**
+	 * Name of the branch this commit was made in. Returns "default" for main branch.
+	 * @return name of the branch, non-empty string
+	 */
 	public String getBranch() {
 		return changeset.branch();
 	}
@@ -100,10 +134,26 @@
 	public HgDate getDate() {
 		return new HgDate(changeset.date().getTime(), changeset.timezone());
 	}
+
+	/**
+	 * Indicates revision of manifest that tracks state of repository at the moment of this commit.
+	 * Note, may be {@link Nodeid#NULL} in certain scenarios (e.g. first changeset in an empty repository, usually by bogus tools)
+	 *  
+	 * @return revision identifier, never <code>null</code>
+	 */
 	public Nodeid getManifestRevision() {
 		return changeset.manifest();
 	}
 
+	/**
+	 * Lists names of files affected by this commit, as recorded in the changeset itself. Unlike {@link #getAddedFiles()}, 
+	 * {@link #getModifiedFiles()} and {@link #getRemovedFiles()}, this method doesn't analyze actual changes done 
+	 * in the commit, rather extracts value from the changeset record.
+	 * 
+	 * List returned by this method may be empty, while aforementioned methods may produce non-empty result.
+	 *   
+	 * @return list of filenames, never <code>null</code>
+	 */
 	public List<Path> getAffectedFiles() {
 		// reports files as recorded in changelog. Note, merge revisions may have no
 		// files listed, and thus this method would return empty list, while
@@ -116,21 +166,42 @@
 		return rv;
 	}
 
-	public List<HgFileRevision> getModifiedFiles() throws HgInvalidControlFileException {
+	/**
+	 * Figures out files and specific revisions thereof that were modified in this commit
+	 *  
+	 * @return revisions of files modified in this commit
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+ 	 * @throws HgException in case of some other library issue 
+	 */
+	public List<HgFileRevision> getModifiedFiles() throws HgException {
 		if (modifiedFiles == null) {
 			initFileChanges();
 		}
 		return modifiedFiles;
 	}
 
-	public List<HgFileRevision> getAddedFiles() throws HgInvalidControlFileException {
+	/**
+	 * Figures out files added in this commit
+	 * 
+	 * @return revisions of files added in this commit
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgException in case of some other library issue 
+	 */
+	public List<HgFileRevision> getAddedFiles() throws HgException {
 		if (addedFiles == null) {
 			initFileChanges();
 		}
 		return addedFiles;
 	}
 
-	public List<Path> getRemovedFiles() throws HgInvalidControlFileException {
+	/**
+	 * Figures out files that were deleted as part of this commit
+	 * 
+	 * @return revisions of files deleted in this commit
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgException in case of some other library issue 
+	 */
+	public List<Path> getRemovedFiles() throws HgException {
 		if (deletedFiles == null) {
 			initFileChanges();
 		}
@@ -175,6 +246,9 @@
 		return Nodeid.fromBinary(parent2, 0);
 	}
 
+	/**
+	 * Create a copy of this changeset 
+	 */
 	@Override
 	public HgChangeset clone() {
 		try {
@@ -186,7 +260,7 @@
 		}
 	}
 
-	private /*synchronized*/ void initFileChanges() throws HgInvalidControlFileException {
+	private /*synchronized*/ void initFileChanges() throws HgException {
 		ArrayList<Path> deleted = new ArrayList<Path>();
 		ArrayList<HgFileRevision> modified = new ArrayList<HgFileRevision>();
 		ArrayList<HgFileRevision> added = new ArrayList<HgFileRevision>();
@@ -196,7 +270,7 @@
 		for (Path s : r.getModified()) {
 			Nodeid nid = r.nodeidAfterChange(s);
 			if (nid == null) {
-				throw new HgBadStateException();
+				throw new HgException(String.format("For the file %s recorded as modified couldn't find revision after change", s));
 			}
 			modified.add(new HgFileRevision(repo, nid, s, null));
 		}
@@ -204,7 +278,7 @@
 		for (Path s : r.getAdded()) {
 			Nodeid nid = r.nodeidAfterChange(s);
 			if (nid == null) {
-				throw new HgBadStateException();
+				throw new HgException(String.format("For the file %s recorded as added couldn't find revision after change", s));
 			}
 			added.add(new HgFileRevision(repo, nid, s, copied.get(s)));
 		}
--- a/src/org/tmatesoft/hg/core/HgChangesetTreeHandler.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgChangesetTreeHandler.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,7 +22,10 @@
 import org.tmatesoft.hg.util.Pair;
 
 /**
- *
+ * Handler to iterate file history (generally, any revlog) with access to parent-child relations between changesets.
+ * 
+ * @see HgLogCommand#execute(HgChangesetTreeHandler)
+ * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
@@ -30,55 +33,60 @@
 	/**
 	 * @param entry access to various pieces of information about current tree node. Instances might be 
 	 * reused across calls and shall not be kept by client's code
+	 * @throws HgException allows implementers propagate errors from {@link TreeElement} or other parts of the library.
 	 * @throws HgCallbackTargetException.Wrap wrapper object for any exception user code may produce. Wrapped exception would get re-thrown with {@link HgCallbackTargetException} 
 	 * @throws CancelledException if execution of the operation was cancelled
 	 */
-	public void next(HgChangesetTreeHandler.TreeElement entry) throws HgCallbackTargetException.Wrap, CancelledException;
+	public void next(HgChangesetTreeHandler.TreeElement entry) throws HgException, HgCallbackTargetException.Wrap, CancelledException;
 
 	interface TreeElement {
 		/**
 		 * Revision of the revlog being iterated. For example, when walking file history, return value represents file revisions.
 		 * 
 		 * @return revision of the revlog being iterated.
+		 * @throws HgException to indicate failure dealing with Mercurial data
 		 */
-		public Nodeid fileRevision();
+		public Nodeid fileRevision() throws HgException;
 
 		/**
 		 * @return changeset associated with the current revision
-		 * @throws HgException indicates failure dealing with Mercurial data
+		 * @throws HgException to indicate failure dealing with Mercurial data
 		 */
 		public HgChangeset changeset() throws HgException;
 
 		/**
 		 * Lightweight alternative to {@link #changeset()}, identifies changeset in which current file node has been modified 
-		 * @return changeset {@link Nodeid} 
+		 * @return changeset {@link Nodeid revision} 
+		 * @throws HgException to indicate failure dealing with Mercurial data
 		 */
-		public Nodeid changesetRevision();
+		public Nodeid changesetRevision() throws HgException;
 
 		/**
 		 * Node, these are not necessarily in direct relation to parents of changeset from {@link #changeset()} 
 		 * @return changesets that correspond to parents of the current file node, either pair element may be <code>null</code>.
-		 * @throws HgException indicates failure dealing with Mercurial data
+		 * @throws HgException to indicate failure dealing with Mercurial data
 		 */
 		public Pair<HgChangeset, HgChangeset> parents() throws HgException;
 		
 		/**
 		 * Lightweight alternative to {@link #parents()}, give {@link Nodeid nodeids} only
 		 * @return two values, neither is <code>null</code>, use {@link Nodeid#isNull()} to identify parent not set
+		 * @throws HgException to indicate failure dealing with Mercurial data
 		 */
-		public Pair<Nodeid, Nodeid> parentRevisions();
+		public Pair<Nodeid, Nodeid> parentRevisions() throws HgException;
 
 		/**
 		 * Changes that originate from the given change and bear it as their parent. 
 		 * @return collection (possibly empty) of immediate children of the change
-		 * @throws HgException indicates failure dealing with Mercurial data
+		 * @throws HgException to indicate failure dealing with Mercurial data
 		 */
 		public Collection<HgChangeset> children() throws HgException;
 
 		/**
 		 * Lightweight alternative to {@link #children()}.
 		 * @return never <code>null</code>
+		 * @throws HgException to indicate failure dealing with Mercurial data
 		 */
-		public Collection<Nodeid> childRevisions();
+		public Collection<Nodeid> childRevisions() throws HgException;
 	}
 }
\ No newline at end of file
--- a/src/org/tmatesoft/hg/core/HgDataStreamException.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgDataStreamException.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,12 +21,14 @@
 
 /**
  * Any erroneous state with @link {@link HgDataFile} input/output, read/write operations
- * FIXME/REVISIT if HgInvalidControlFileExceptio and HgInvalidFileException is not sufficient? Is there real need for all 3?  
+ * 
+ * @deprecated {@link HgInvalidControlFileException} and {@link HgInvalidFileException} deemed sufficient
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
 @SuppressWarnings("serial")
+@Deprecated
 public class HgDataStreamException extends HgException {
 
 	public HgDataStreamException(Path file, String message, Throwable cause) {
--- a/src/org/tmatesoft/hg/core/HgException.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgException.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,8 +16,7 @@
  */
 package org.tmatesoft.hg.core;
 
-import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
-
+import org.tmatesoft.hg.internal.ExceptionInfo;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.Path;
 
@@ -29,10 +28,8 @@
  */
 @SuppressWarnings("serial")
 public class HgException extends Exception {
-
-	protected int revNumber = BAD_REVISION;
-	protected Nodeid revision;
-	protected Path filename;
+	
+	protected final ExceptionInfo<HgException> extras = new ExceptionInfo<HgException>(this);
 
 	public HgException(String reason) {
 		super(reason);
@@ -50,7 +47,7 @@
 	 * @return not {@link HgRepository#BAD_REVISION} only when revision index was supplied at the construction time
 	 */
 	public int getRevisionIndex() {
-		return revNumber;
+		return extras.getRevisionIndex();
 	}
 
 	/**
@@ -61,12 +58,14 @@
 		return getRevisionIndex();
 	}
 	
-
 	public HgException setRevisionIndex(int rev) {
-		revNumber = rev;
-		return this;
+		return extras.setRevisionIndex(rev);
 	}
 	
+	public boolean isRevisionIndexSet() {
+		return extras.isRevisionIndexSet();
+	}
+
 	/**
 	 * @deprecated use {@link #setRevisionIndex(int)}
 	 */
@@ -79,44 +78,26 @@
 	 * @return non-null only when revision was supplied at construction time
 	 */
 	public Nodeid getRevision() {
-		return revision;
+		return extras.getRevision();
 	}
 
 	public HgException setRevision(Nodeid r) {
-		revision = r;
-		return this;
+		return extras.setRevision(r);
+	}
+	
+	public boolean isRevisionSet() {
+		return extras.isRevisionSet();
 	}
 
 	/**
 	 * @return non-null only if file name was set at construction time
 	 */
 	public Path getFileName() {
-		return filename;
+		return extras.getFileName();
 	}
 
 	public HgException setFileName(Path name) {
-		filename = name;
-		return this;
-	}
-	
-	protected void appendDetails(StringBuilder sb) {
-		if (filename != null) {
-			sb.append("file:'");
-			sb.append(filename);
-			sb.append('\'');
-			sb.append(';');
-			sb.append(' ');
-		}
-		sb.append("rev:");
-		if (revNumber != BAD_REVISION) {
-			sb.append(revNumber);
-			if (revision != null) {
-				sb.append(':');
-			}
-		}
-		if (revision != null) {
-			sb.append(revision.shortNotation());
-		}
+		return extras.setFileName(name);
 	}
 
 	@Override
@@ -124,7 +105,7 @@
 		StringBuilder sb = new StringBuilder(super.toString());
 		sb.append(' ');
 		sb.append('(');
-		appendDetails(sb);
+		extras.appendDetails(sb);
 		sb.append(')');
 		return sb.toString();
 	}
--- a/src/org/tmatesoft/hg/core/HgFileInformer.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgFileInformer.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@
 
 import org.tmatesoft.hg.internal.ManifestRevision;
 import org.tmatesoft.hg.repo.HgDataFile;
-import org.tmatesoft.hg.repo.HgInternals;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.Path;
 import org.tmatesoft.hg.util.Status;
@@ -122,6 +121,7 @@
 			return checkResult;
 		}
 		Nodeid toExtract = null;
+		String phaseMsg = "Extract manifest revision failed";
 		try {
 			if (cachedManifest == null) {
 				int csetRev = repo.getChangelog().getRevisionIndex(cset);
@@ -130,6 +130,7 @@
 				// cachedManifest shall be meaningful - changelog.getRevisionIndex() above ensures we've got version that exists.
 			}
 			toExtract = cachedManifest.nodeid(file);
+			phaseMsg = "Follow copy/rename failed";
 			if (toExtract == null && followRenames) {
 				while (toExtract == null && dataFile.isCopy()) {
 					renamed = true;
@@ -138,12 +139,8 @@
 					toExtract = cachedManifest.nodeid(file);
 				}
 			}
-		} catch (HgInvalidControlFileException ex) {
-			checkResult = new Status(Status.Kind.ERROR, "", ex);
-			return checkResult;
-		} catch (HgDataStreamException ex) {
-			checkResult = new Status(Status.Kind.ERROR, "Follow copy/rename failed", ex);
-			HgInternals.getContext(repo).getLog().warn(getClass(), ex, checkResult.getMessage());
+		} catch (HgException ex) {
+			checkResult = new Status(Status.Kind.ERROR, phaseMsg, ex);
 			return checkResult;
 		}
 		if (toExtract != null) {
--- a/src/org/tmatesoft/hg/core/HgFileRevision.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgFileRevision.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -99,13 +99,13 @@
 		return parents;
 	}
 
-	public void putContentTo(ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException {
+	public void putContentTo(ByteChannel sink) throws HgException, CancelledException {
 		HgDataFile fn = repo.getFileNode(path);
 		int revisionIndex = fn.getRevisionIndex(revision);
 		fn.contentWithFilters(revisionIndex, sink);
 	}
 
-	private void checkCopy() throws HgInvalidControlFileException, HgDataStreamException {
+	private void checkCopy() throws HgException {
 		HgDataFile fn = repo.getFileNode(path);
 		if (fn.isCopy()) {
 			if (fn.getRevision(0).equals(revision)) {
--- a/src/org/tmatesoft/hg/core/HgInvalidControlFileException.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgInvalidControlFileException.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,17 +19,20 @@
 import java.io.File;
 
 import org.tmatesoft.hg.internal.Experimental;
+import org.tmatesoft.hg.util.Path;
 
 /**
  * WORK IN PROGRESS
  * 
  * Subclass of {@link HgInvalidFileException} to indicate failure to deal with one of <b>Mercurial</b> control files 
  * (most likely those under .hg/, but also those residing in the repository, with special meaning to the Mercurial, like .hgtags or .hgignore)
+ * 
+ * XXX Perhaps, HgInvalidRevlogException?
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
 @SuppressWarnings("serial")
-@Experimental(reason="WORK IN PROGRESS. Name is likely to change. Path argument to be added?")
+@Experimental(reason="WORK IN PROGRESS. Name is likely to change")
 public class HgInvalidControlFileException extends HgInvalidFileException {
 
 	public HgInvalidControlFileException(String message, Throwable th, File file) {
@@ -44,7 +47,16 @@
 	
 	@Override
 	public HgInvalidControlFileException setRevision(Nodeid r) {
-		super.setRevision(r);
-		return this;
+		return (HgInvalidControlFileException) super.setRevision(r);
+	}
+
+	@Override
+	public HgInvalidControlFileException setRevisionIndex(int rev) {
+		return (HgInvalidControlFileException) super.setRevisionIndex(rev);
+	}
+	
+	@Override
+	public HgInvalidControlFileException setFileName(Path name) {
+		return (HgInvalidControlFileException) super.setFileName(name);
 	}
 }
--- a/src/org/tmatesoft/hg/core/HgInvalidFileException.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgInvalidFileException.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -36,20 +36,18 @@
 @SuppressWarnings("serial")
 public class HgInvalidFileException extends HgException {
 
-	private File localFile;
-
 	public HgInvalidFileException(String message, Throwable th) {
 		super(message, th);
 	}
 
 	public HgInvalidFileException(String message, Throwable th, File file) {
 		super(message, th);
-		localFile = file;
+		extras.setFile(file); // allows null
 	}
 
 	public HgInvalidFileException setFile(File file) {
-		assert file != null;
-		localFile = file;
+		assert file != null; // doesn't allow null not to clear file accidentally
+		extras.setFile(file);
 		return this;
 	}
 
@@ -57,21 +55,6 @@
 	 * @return file object that causes troubles, or <code>null</code> if specific file is unknown
 	 */
 	public File getFile() {
-		return localFile;
-	}
-
-	@Override
-	protected void appendDetails(StringBuilder sb) {
-		super.appendDetails(sb);
-		if (localFile != null) {
-			sb.append(" file:");
-			sb.append(localFile.getPath());
-			sb.append(',');
-			if (localFile.exists()) {
-				sb.append("EXISTS");
-			} else {
-				sb.append("DOESN'T EXIST");
-			}
-		}
+		return extras.getFile();
 	}
 }
--- a/src/org/tmatesoft/hg/core/HgInvalidRevisionException.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgInvalidRevisionException.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,8 +16,9 @@
  */
 package org.tmatesoft.hg.core;
 
+import static org.tmatesoft.hg.repo.HgRepository.*;
+
 import org.tmatesoft.hg.internal.Experimental;
-import org.tmatesoft.hg.repo.HgRepository;
 
 /**
  * Use of revision or revision local index that is not valid for a given revlog.
@@ -29,9 +30,9 @@
 @Experimental(reason="1) Whether to use checked or runtime exception is not yet decided. 2) Perhaps, its use not bound to wrong arguments")
 public class HgInvalidRevisionException extends IllegalArgumentException {
 	private Nodeid rev;
-	private Integer revIdx;
+	private Integer revIdx = BAD_REVISION;
 	// next two make sense only when revIdx is present
-	private int rangeLeftBoundary = -1, rangeRightBoundary = -1;
+	private int rangeLeftBoundary = BAD_REVISION, rangeRightBoundary = BAD_REVISION;
 
 	/**
 	 * 
@@ -75,7 +76,7 @@
 		revIdx = revisionIndex;
 		return this;
 	}
-
+	
 	public HgInvalidRevisionException setRevisionIndex(int revisionIndex, int rangeLeft, int rangeRight) {
 		revIdx = revisionIndex;
 		rangeLeftBoundary = rangeLeft;
@@ -83,6 +84,14 @@
 		return this;
 	}
 
+	public boolean isRevisionSet() {
+		return rev != null;
+	}
+	
+	public boolean isRevisionIndexSet() {
+		return revIdx != BAD_REVISION;
+	}
+
 	@Override
 	public String getMessage() {
 		String msg = super.getMessage();
@@ -98,12 +107,13 @@
 		if (revIdx != null) {
 			String sr;
 			switch (revIdx) {
-			case HgRepository.BAD_REVISION : sr = "UNKNOWN"; break;
-			case HgRepository.TIP : sr = "TIP"; break;
-			case HgRepository.WORKING_COPY: sr = "WORKING-COPY"; break;
+			case BAD_REVISION : sr = "UNKNOWN"; break;
+			case TIP : sr = "TIP"; break;
+			case WORKING_COPY: sr = "WORKING-COPY"; break;
+			case NO_REVISION : sr = "NO REVISION"; break;
 			default : sr = revIdx.toString();
 			}
-			if (rangeLeftBoundary != -1 || rangeRightBoundary != -1) {
+			if (rangeLeftBoundary != BAD_REVISION || rangeRightBoundary != BAD_REVISION) {
 				sb.append(String.format("%s is not from [%d..%d]", sr, rangeLeftBoundary, rangeRightBoundary));
 			} else {
 				sb.append(sr);
--- a/src/org/tmatesoft/hg/core/HgLogCommand.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgLogCommand.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -186,25 +186,24 @@
 	/**
 	 * Similar to {@link #execute(org.tmatesoft.hg.repo.RawChangeset.Inspector)}, collects and return result as a list.
 	 */
-	public List<HgChangeset> execute() throws HgDataStreamException {
+	public List<HgChangeset> execute() throws HgException {
 		CollectHandler collector = new CollectHandler();
 		try {
 			execute(collector);
-		} catch (HgException ex) {
+		} catch (CancelledException ex) {
 			// can't happen as long as our CollectHandler doesn't throw any exception
-			throw new HgBadStateException(ex.getCause());
-		} catch (CancelledException ex) {
-			// can't happen, see above
 			throw new HgBadStateException(ex);
 		}
 		return collector.getChanges();
 	}
 
 	/**
+	 * Iterate over range of changesets configured in the command.
 	 * 
 	 * @param handler callback to process changesets.
 	 * @throws HgCallbackTargetException to re-throw exception from the handler
-	 * @throws HgException FIXME EXCEPTIONS
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgException in case of some other library issue 
 	 * @throws CancelledException if execution of the command was cancelled
 	 * @throws IllegalArgumentException when inspector argument is null
 	 * @throws ConcurrentModificationException if this log command instance is already running
@@ -263,11 +262,15 @@
 	}
 	
 	/**
-	 * TODO documentation 
-	 * @param handler
+	 * Tree-wise iteration of a file history, with handy access to parent-child relations between changesets. 
+	 *  
+	 * @param handler callback to process changesets.
 	 * @throws HgCallbackTargetException to re-throw exception from the handler
-	 * @throws HgException FIXME EXCEPTIONS
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgException in case of some other library issue 
 	 * @throws CancelledException if execution of the command was cancelled
+	 * @throws IllegalArgumentException if command is not satisfied with its arguments 
+	 * @throws ConcurrentModificationException if this log command instance is already running
 	 */
 	public void execute(HgChangesetTreeHandler handler) throws HgCallbackTargetException, HgException, CancelledException {
 		if (handler == null) {
@@ -510,22 +513,22 @@
 		}
 		
 		void populate(HgChangeset cs) {
-			cachedChangesets.put(cs.getRevision(), cs);
+			cachedChangesets.put(cs.getRevisionIndex(), cs);
 		}
 		
-		private HgChangeset[] get(int... changelogRevisionNumber) throws HgInvalidControlFileException {
-			HgChangeset[] rv = new HgChangeset[changelogRevisionNumber.length];
-			IntVector misses = new IntVector(changelogRevisionNumber.length, -1);
-			for (int i = 0; i < changelogRevisionNumber.length; i++) {
-				if (changelogRevisionNumber[i] == -1) {
+		private HgChangeset[] get(int... changelogRevisionIndex) throws HgException {
+			HgChangeset[] rv = new HgChangeset[changelogRevisionIndex.length];
+			IntVector misses = new IntVector(changelogRevisionIndex.length, -1);
+			for (int i = 0; i < changelogRevisionIndex.length; i++) {
+				if (changelogRevisionIndex[i] == -1) {
 					rv[i] = null;
 					continue;
 				}
-				HgChangeset cached = cachedChangesets.get(changelogRevisionNumber[i]);
+				HgChangeset cached = cachedChangesets.get(changelogRevisionIndex[i]);
 				if (cached != null) {
 					rv[i] = cached;
 				} else {
-					misses.add(changelogRevisionNumber[i]);
+					misses.add(changelogRevisionIndex[i]);
 				}
 			}
 			if (misses.size() > 0) {
@@ -534,20 +537,23 @@
 				repo.getChangelog().range(this, changesets2read);
 				for (int changeset2read : changesets2read) {
 					HgChangeset cs = cachedChangesets.get(changeset2read);
-						if (cs == null) {
-							throw new HgBadStateException();
+					if (cs == null) {
+						throw new HgException(String.format("Can't get changeset for revision %d", changeset2read));
+					}
+					// HgChangelog.range may reorder changesets according to their order in the changelog
+					// thus need to find original index
+					boolean sanity = false;
+					for (int i = 0; i < changelogRevisionIndex.length; i++) {
+						if (changelogRevisionIndex[i] == cs.getRevisionIndex()) {
+							rv[i] = cs;
+							sanity = true;
+							break;
 						}
-						// HgChangelog.range may reorder changesets according to their order in the changelog
-						// thus need to find original index
-						boolean sanity = false;
-						for (int i = 0; i < changelogRevisionNumber.length; i++) {
-							if (changelogRevisionNumber[i] == cs.getRevision()) {
-								rv[i] = cs;
-								sanity = true;
-								break;
-							}
-						}
-						assert sanity;
+					}
+					if (!sanity) {
+						HgInternals.getContext(repo).getLog().error(getClass(), "Index of revision %d:%s doesn't match any of requested", cs.getRevisionIndex(), cs.getNodeid().shortNotation());
+					}
+					assert sanity;
 				}
 			}
 			return rv;
@@ -565,14 +571,14 @@
 			populate(cs.clone());
 		}
 
-		public Nodeid changesetRevision() {
+		public Nodeid changesetRevision() throws HgException {
 			if (changesetRevision == null) {
 				changesetRevision = getRevision(historyNode.changeset);
 			}
 			return changesetRevision;
 		}
 
-		public Pair<Nodeid, Nodeid> parentRevisions() {
+		public Pair<Nodeid, Nodeid> parentRevisions() throws HgException {
 			if (parentRevisions == null) {
 				HistoryNode p;
 				final Nodeid p1, p2;
@@ -591,7 +597,7 @@
 			return parentRevisions;
 		}
 
-		public Collection<Nodeid> childRevisions() {
+		public Collection<Nodeid> childRevisions() throws HgException {
 			if (childRevisions != null) {
 				return childRevisions;
 			}
@@ -608,19 +614,13 @@
 		}
 		
 		// reading nodeid involves reading index only, guess, can afford not to optimize multiple reads
-		private Nodeid getRevision(int changelogRevisionNumber) {
-			// XXX pipe through pool
+		private Nodeid getRevision(int changelogRevisionNumber) throws HgInvalidControlFileException {
+			// TODO [post-1.0] pipe through pool
 			HgChangeset cs = cachedChangesets.get(changelogRevisionNumber);
 			if (cs != null) {
 				return cs.getNodeid();
 			} else {
-				try {
-					return repo.getChangelog().getRevision(changelogRevisionNumber);
-				} catch (HgException ex) {
-					HgInternals.getContext(repo).getLog().error(getClass(), ex, null);
-					// FIXME propagate, perhaps?
-					return Nodeid.NULL; // FIXME this is quick-n-dirty hack to move forward with introducing exceptions 
-				}
+				return repo.getChangelog().getRevision(changelogRevisionNumber);
 			}
 		}
 	}
--- a/src/org/tmatesoft/hg/core/package.html	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/core/package.html	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,6 @@
 <html>
-<boody>
-Hi-level API
-</bidy>
+<body>
+<h2>Hi-level API</h2>
+<p>Hi-level API to deal with Mercurial repositories using task-oriented commands. Start with {@link HgRepoFacade} class</p>
+</body>
 </html>
\ No newline at end of file
--- a/src/org/tmatesoft/hg/internal/DataAccessProvider.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/DataAccessProvider.java	Wed Mar 21 20:51:12 2012 +0100
@@ -317,6 +317,7 @@
 				try {
 					fileChannel.close();
 				} catch (IOException ex) {
+					// FIXME/TODO log facility can be obtained from session context 
 					StreamLogFacility.newDefault().debug(getClass(), ex, null);
 				}
 				fileChannel = null;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/ExceptionInfo.java	Wed Mar 21 20:51:12 2012 +0100
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2012 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.internal;
+
+import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
+
+import java.io.File;
+
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.util.Path;
+
+/**
+ * Extras to record with exception to describe it better.
+ * XXX perhaps, not only with exception, may utilize it with status object? 
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class ExceptionInfo<T> {
+	protected final T owner;
+	protected int revNumber = BAD_REVISION;
+	protected Nodeid revision;
+	protected Path filename;
+	protected File localFile;
+
+	/**
+	 * @param owner instance to return from setters 
+	 */
+	public ExceptionInfo(T owner) {
+		this.owner = owner;
+	}
+	
+	/**
+	 * @return not {@link HgRepository#BAD_REVISION} only when revision index was supplied at the construction time
+	 */
+	public int getRevisionIndex() {
+		return revNumber;
+	}
+
+	public T setRevisionIndex(int rev) {
+		revNumber = rev;
+		return owner;
+	}
+	
+	public boolean isRevisionIndexSet() {
+		return revNumber != BAD_REVISION;
+	}
+
+	/**
+	 * @return non-null only when revision was supplied at construction time
+	 */
+	public Nodeid getRevision() {
+		return revision;
+	}
+
+	public T setRevision(Nodeid r) {
+		revision = r;
+		return owner;
+	}
+	
+	public boolean isRevisionSet() {
+		return revision != null;
+	}
+
+	/**
+	 * @return non-null only if file name was set at construction time
+	 */
+	public Path getFileName() {
+		return filename;
+	}
+
+	public T setFileName(Path name) {
+		filename = name;
+		return owner;
+	}
+
+	public T setFile(File file) {
+		localFile = file;
+		return owner;
+	}
+
+	/**
+	 * @return file object that causes troubles, or <code>null</code> if specific file is unknown
+	 */
+	public File getFile() {
+		return localFile;
+	}
+
+	public StringBuilder appendDetails(StringBuilder sb) {
+		if (filename != null) {
+			sb.append("path:'");
+			sb.append(filename);
+			sb.append('\'');
+			sb.append(';');
+			sb.append(' ');
+		}
+		sb.append("rev:");
+		if (revNumber != BAD_REVISION) {
+			sb.append(revNumber);
+			if (revision != null) {
+				sb.append(':');
+			}
+		}
+		if (revision != null) {
+			sb.append(revision.shortNotation());
+		}
+		if (localFile != null) {
+			sb.append(';');
+			sb.append(' ');
+			sb.append(" file:");
+			sb.append(localFile.getPath());
+			sb.append(',');
+			if (localFile.exists()) {
+				sb.append("EXISTS");
+			} else {
+				sb.append("DOESN'T EXIST");
+			}
+		}
+		return sb;
+	}
+}
--- a/src/org/tmatesoft/hg/internal/Internals.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/Internals.java	Wed Mar 21 20:51:12 2012 +0100
@@ -195,6 +195,29 @@
 	}
 	
 	/**
+	 * @param hint optional hint pointing to filesystem of interest (generally, it's possible to mount 
+	 * filesystems with different capabilities and repository's capabilities would depend on which fs it resides) 
+	 * @return <code>true</code> if executable files deserve tailored handling 
+	 */
+	public static boolean checkSupportsExecutables(File fsHint) {
+		// *.exe are not executables for Mercurial
+		return !runningOnWindows();
+	}
+
+	/**
+	 * @param hint optional hint pointing to filesystem of interest (generally, it's possible to mount 
+	 * filesystems with different capabilities and repository's capabilities would depend on which fs it resides) 
+	 * @return <code>true</code> if filesystem knows what symbolic links are 
+	 */
+	public static boolean checkSupportsSymlinks(File fsHint) {
+		// Windows supports soft symbolic links starting from Vista 
+		// However, as of Mercurial 2.1.1, no support for this functionality
+		// XXX perhaps, makes sense to override with a property a) to speed up when no links are in use b) investigate how this runs windows
+		return !runningOnWindows();
+	}
+
+	
+	/**
 	 * For Unix, returns installation root, which is the parent directory of the hg executable (or symlink) being run.
 	 * For Windows, it's Mercurial installation directory itself 
 	 * @param ctx 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/ProcessExecHelper.java	Wed Mar 21 20:51:12 2012 +0100
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2012 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.internal;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.CharBuffer;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Utility to run shell commands. Not thread-safe.
+ * Beware of memory overcommitment issue on Linux - suprocess get allocated virtual memory of parent process size
+ * @see http://developers.sun.com/solaris/articles/subprocess/subprocess.html
+ * 
+ * @author Artem Tikhomirov
+ * @author Tmate Software Ltd.
+ */
+public class ProcessExecHelper {
+	private File dir;
+	private int exitValue;
+	private ProcessBuilder pb;
+	
+	public ProcessExecHelper() {
+	}
+	
+	protected List<String> prepareCommand(List<String> cmd) {
+		return cmd;
+	}
+	
+	public CharSequence exec(String... command) throws IOException, InterruptedException {
+		return exec(Arrays.asList(command));
+	}
+
+	public CharSequence exec(List<String> command) throws IOException, InterruptedException {
+		List<String> cmd = prepareCommand(command);
+		if (pb == null) {
+			pb = new ProcessBuilder(cmd).directory(dir).redirectErrorStream(true);
+		} else {
+			pb.command(cmd); // dir and redirect are set
+		}
+		Process p = pb.start();
+		InputStreamReader stdOut = new InputStreamReader(p.getInputStream());
+		LinkedList<CharBuffer> l = new LinkedList<CharBuffer>();
+		int r = -1;
+		CharBuffer b = null;
+		do {
+			if (b == null || b.remaining() < b.capacity() / 3) {
+				b = CharBuffer.allocate(512);
+				l.add(b);
+			}
+			r = stdOut.read(b);
+		} while (r != -1);
+		int total = 0;
+		for (CharBuffer cb : l) {
+			total += cb.position();
+			cb.flip();
+		}
+		CharBuffer res = CharBuffer.allocate(total);
+		for (CharBuffer cb : l) {
+			res.put(cb);
+		}
+		res.flip();
+		p.waitFor();
+		exitValue = p.exitValue();
+		return res;
+	}
+	
+	public int exitValue() {
+		return exitValue;
+	}
+
+	public ProcessExecHelper cwd(File wd) {
+		dir = wd;
+		if (pb != null) {
+			pb.directory(dir);
+		}
+		return this;
+	}
+}
--- a/src/org/tmatesoft/hg/internal/RevlogDump.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/RevlogDump.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2011 TMate Software Ltd
+ * Copyright (c) 2010-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
--- a/src/org/tmatesoft/hg/internal/RevlogStream.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/RevlogStream.java	Wed Mar 21 20:51:12 2012 +0100
@@ -56,6 +56,7 @@
 	private int[] baseRevisions;
 	private boolean inline = false;
 	private final File indexFile;
+	private File dataFile;
 	private final DataAccessProvider dataAccess;
 
 	// if we need anything else from HgRepo, might replace DAP parameter with HgRepo and query it for DAP.
@@ -71,11 +72,34 @@
 	}
 
 	/*package*/ DataAccess getDataStream() {
-		final String indexName = indexFile.getName();
-		File dataFile = new File(indexFile.getParentFile(), indexName.substring(0, indexName.length() - 1) + "d");
-		return dataAccess.create(dataFile);
+		return dataAccess.create(getDataFile());
 	}
 	
+	/**
+	 * Constructs file object that corresponds to .d revlog counterpart. 
+	 * Note, it's caller responsibility to ensure this file makes any sense (i.e. check {@link #inline} attribute)
+	 */
+	private File getDataFile() {
+		if (dataFile == null) {
+			final String indexName = indexFile.getName();
+			dataFile = new File(indexFile.getParentFile(), indexName.substring(0, indexName.length() - 1) + "d");
+		}
+		return dataFile;
+	}
+	
+	// initialize exception with the file where revlog structure information comes from
+	public HgInvalidControlFileException initWithIndexFile(HgInvalidControlFileException ex) {
+		return ex.setFile(indexFile);
+	}
+
+	// initialize exception with the file where revlog data comes from
+	public HgInvalidControlFileException initWithDataFile(HgInvalidControlFileException ex) {
+		// exceptions are usually raised after read attepmt, hence inline shall be initialized
+		// although honest approach is to call #initOutline() first
+		return ex.setFile(inline ? indexFile : getDataFile());
+	}
+
+	
 	public int revisionCount() {
 		initOutline();
 		return baseRevisions.length;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/package.html	Wed Mar 21 20:51:12 2012 +0100
@@ -0,0 +1,5 @@
+<html>
+<body>
+Intimate implementation peculiarities of the library. Classes in this package shall be deemed internal API and not referenced from outside unless you really known what you do and what are possible consequences. 
+</body>
+</html>
\ No newline at end of file
--- a/src/org/tmatesoft/hg/repo/HgDataFile.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgDataFile.java	Wed Mar 21 20:51:12 2012 +0100
@@ -31,9 +31,9 @@
 import java.util.Collections;
 import java.util.List;
 
-import org.tmatesoft.hg.core.HgDataStreamException;
 import org.tmatesoft.hg.core.HgException;
 import org.tmatesoft.hg.core.HgInvalidControlFileException;
+import org.tmatesoft.hg.core.HgInvalidFileException;
 import org.tmatesoft.hg.core.HgInvalidRevisionException;
 import org.tmatesoft.hg.core.HgLogCommand;
 import org.tmatesoft.hg.core.Nodeid;
@@ -93,23 +93,42 @@
 	 * @param nodeid revision of the file
 	 * 
 	 * @return size of the file content at the given revision
-	 * @throws HgInvalidRevisionException if supplied nodeid doesn't identify any revision from this revlog  
-	 * @throws HgDataStreamException if attempt to access file metadata failed
+	 * @throws HgInvalidRevisionException if supplied nodeid doesn't identify any revision from this revlog (<em>runtime exception</em>)  
 	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
 	 */
-	public int length(Nodeid nodeid) throws HgDataStreamException, HgInvalidControlFileException, HgInvalidRevisionException {
-		return length(getRevisionIndex(nodeid));
+	public int length(Nodeid nodeid) throws HgInvalidControlFileException, HgInvalidRevisionException {
+		try {
+			return length(getRevisionIndex(nodeid));
+		} catch (HgInvalidControlFileException ex) {
+			throw ex.isRevisionSet() ? ex : ex.setRevision(nodeid);
+		} catch (HgInvalidRevisionException ex) {
+			throw ex.isRevisionSet() ? ex : ex.setRevision(nodeid);
+		}
 	}
 	
 	/**
  	 * @param fileRevisionIndex - revision local index, non-negative. From predefined constants, only {@link HgRepository#TIP} makes sense. 
 	 * @return size of the file content at the revision identified by local revision number.
-	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog
-	 * @throws HgDataStreamException if attempt to access file metadata failed
+	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog (<em>runtime exception</em>)
 	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
 	 */
-	public int length(int fileRevisionIndex) throws HgDataStreamException, HgInvalidControlFileException, HgInvalidRevisionException {
-		// TODO support WORKING_COPY constant
+	public int length(int fileRevisionIndex) throws HgInvalidControlFileException, HgInvalidRevisionException {
+		if (wrongRevisionIndex(fileRevisionIndex) || fileRevisionIndex == BAD_REVISION) {
+			throw new HgInvalidRevisionException(fileRevisionIndex);
+		}
+		if (fileRevisionIndex == TIP) {
+			fileRevisionIndex = getLastRevision();
+		} else if (fileRevisionIndex == WORKING_COPY) {
+			File f = getRepo().getFile(this);
+			if (f.exists()) {
+				return (int) /*FIXME long!*/ f.length();
+			}
+			Nodeid fileRev = getWorkingCopyRevision();
+			if (fileRev == null) {
+				throw new HgInvalidRevisionException(String.format("File %s is not part of working copy", getPath()), null, fileRevisionIndex);
+			}
+			fileRevisionIndex = getRevisionIndex(fileRev);
+		}
 		if (metadata == null || !metadata.checked(fileRevisionIndex)) {
 			checkAndRecordMetadata(fileRevisionIndex);
 		}
@@ -123,14 +142,18 @@
 	/**
 	 * Reads content of the file from working directory. If file present in the working directory, its actual content without
 	 * any filters is supplied through the sink. If file does not exist in the working dir, this method provides content of a file 
-	 * as if it would be refreshed in the working copy, i.e. its corresponding revision 
-	 * (XXX according to dirstate? file tip?) is read from the repository, and filters repo -> working copy get applied.
+	 * as if it would be refreshed in the working copy, i.e. its corresponding revision (according to dirstate) is read from the 
+	 * repository, and filters repo -> working copy get applied.
+	 * 
+	 * NOTE, if file is missing from the working directory and is not part of the dirstate (but otherwise legal repository file,
+	 * e.g. from another branch), no content would be supplied.
 	 *     
-	 * @param sink where to pipe content to
-	 * @throws HgDataStreamException to indicate troubles reading repository file
+	 * @param sink content consumer
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgInvalidFileException if access to file in working directory failed
 	 * @throws CancelledException if execution of the operation was cancelled
 	 */
-	public void workingCopy(ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException {
+	public void workingCopy(ByteChannel sink) throws HgException, CancelledException {
 		File f = getRepo().getFile(this);
 		if (f.exists()) {
 			final CancelSupport cs = CancelSupport.Factory.get(sink);
@@ -150,7 +173,7 @@
 					buf.compact();
 				}
 			} catch (IOException ex) {
-				throw new HgDataStreamException(getPath(), ex);
+				throw new HgInvalidFileException("Working copy read failed", ex, f);
 			} finally {
 				progress.done();
 				if (fc != null) {
@@ -162,71 +185,85 @@
 				}
 			}
 		} else {
-			final Pair<Nodeid, Nodeid> wcParents = getRepo().getWorkingCopyParents();
-			Nodeid p = wcParents.first().isNull() ? wcParents.second() : wcParents.first();
-			if (p.isNull()) {
-				// no dirstate parents - no content 
+			Nodeid fileRev = getWorkingCopyRevision();
+			if (fileRev == null) {
+				// no content for this data file in the working copy - it is not part of the actual working state.
+				// XXX perhaps, shall report this to caller somehow, not silently pass no data?
 				return;
 			}
-			final HgChangelog clog = getRepo().getChangelog();
+			final int fileRevIndex = getRevisionIndex(fileRev);
+			contentWithFilters(fileRevIndex, sink);
+		}
+	}
+	
+	/**
+	 * @return file revision as recorded in repository manifest for dirstate parent, or <code>null</code> if no file revision can be found 
+	 */
+	private Nodeid getWorkingCopyRevision() throws HgInvalidControlFileException {
+		final Pair<Nodeid, Nodeid> wcParents = getRepo().getWorkingCopyParents();
+		Nodeid p = wcParents.first().isNull() ? wcParents.second() : wcParents.first();
+		final HgChangelog clog = getRepo().getChangelog();
+		final int csetRevIndex;
+		if (p.isNull()) {
+			// no dirstate parents 
+			getRepo().getContext().getLog().info(getClass(), "No dirstate parents, resort to TIP", getPath());
+			// if it's a repository with no dirstate, use TIP then
+			csetRevIndex = clog.getLastRevision();
+			if (csetRevIndex == -1) {
+				// shall not happen provided there's .i for this data file (hence at least one cset)
+				// and perhaps exception is better here. However, null as "can't find" indication seems reasonable.
+				return null;
+			}
+		} else {
 			// common case to avoid searching complete changelog for nodeid match
 			final Nodeid tipRev = clog.getRevision(TIP);
-			final int csetRevIndex;
 			if (tipRev.equals(p)) {
 				csetRevIndex = clog.getLastRevision();
 			} else {
 				// bad luck, need to search honestly
 				csetRevIndex = getRepo().getChangelog().getRevisionIndex(p);
 			}
-			Nodeid fileRev = getRepo().getManifest().getFileRevision(csetRevIndex, getPath());
-			final int fileRevIndex = getRevisionIndex(fileRev);
-			contentWithFilters(fileRevIndex, sink);
 		}
+		Nodeid fileRev = getRepo().getManifest().getFileRevision(csetRevIndex, getPath());
+		// it's possible for a file to be in working dir and have store/.i but to belong e.g. to a different
+		// branch than the one from dirstate. Thus it's possible to get null fileRev
+		// which would serve as an indication this data file is not part of working copy
+		return fileRev;
 	}
 	
-//	public void content(int revision, ByteChannel sink, boolean applyFilters) throws HgDataStreamException, IOException, CancelledException {
-//		byte[] content = content(revision);
-//		final CancelSupport cancelSupport = CancelSupport.Factory.get(sink);
-//		final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink);
-//		ByteBuffer buf = ByteBuffer.allocate(512);
-//		int left = content.length;
-//		progressSupport.start(left);
-//		int offset = 0;
-//		cancelSupport.checkCancelled();
-//		ByteChannel _sink = applyFilters ? new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())) : sink;
-//		do {
-//			buf.put(content, offset, Math.min(left, buf.remaining()));
-//			buf.flip();
-//			cancelSupport.checkCancelled();
-//			// XXX I may not rely on returned number of bytes but track change in buf position instead.
-//			int consumed = _sink.write(buf);
-//			buf.compact();
-//			offset += consumed;
-//			left -= consumed;
-//			progressSupport.worked(consumed);
-//		} while (left > 0);
-//		progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully.
-//	}
-	
-	/*XXX not sure distinct method contentWithFilters() is the best way to do, perhaps, callers shall add filters themselves?*/
-	public void contentWithFilters(int revision, ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException, HgInvalidRevisionException {
-		if (revision == WORKING_COPY) {
+	/**
+	 * Access content of a file revision
+	 * XXX not sure distinct method contentWithFilters() is the best way to do, perhaps, callers shall add filters themselves?
+	 * 
+ 	 * @param fileRevisionIndex - revision local index, non-negative. From predefined constants, {@link HgRepository#TIP} and {@link HgRepository#WORKING_COPY} make sense. 
+	 * @param sink content consumer
+	 * 
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgInvalidFileException if access to file in working directory failed
+	 * @throws CancelledException if execution of the operation was cancelled
+	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog (<em>runtime exception</em>)
+	 */
+	public void contentWithFilters(int fileRevisionIndex, ByteChannel sink) throws HgException, CancelledException, HgInvalidRevisionException {
+		if (fileRevisionIndex == WORKING_COPY) {
 			workingCopy(sink); // pass un-mangled sink
 		} else {
-			content(revision, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())));
+			content(fileRevisionIndex, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())));
 		}
 	}
 
 	/**
+	 * Retrieve content of specific revision. Content is provided as is, without any filters (e.g. keywords, eol, etc.) applied.
+	 * For filtered content, use {@link #contentWithFilters(int, ByteChannel)}. 
 	 * 
  	 * @param fileRevisionIndex - revision local index, non-negative. From predefined constants, {@link HgRepository#TIP} and {@link HgRepository#WORKING_COPY} make sense. 
-	 * @param sink
-	 * @throws HgDataStreamException FIXME EXCEPTIONS
+	 * @param sink content consumer
+	 * 
 	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgInvalidFileException if access to file in working directory failed
 	 * @throws CancelledException if execution of the operation was cancelled
-	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog
+	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog (<em>runtime exception</em>)
 	 */
-	public void content(int fileRevisionIndex, ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException, HgInvalidRevisionException {
+	public void content(int fileRevisionIndex, ByteChannel sink) throws HgException, CancelledException, HgInvalidRevisionException {
 		// for data files need to check heading of the file content for possible metadata
 		// @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8-
 		if (fileRevisionIndex == TIP) {
@@ -255,19 +292,22 @@
 			insp = new ContentPipe(sink, metadata.dataOffset(fileRevisionIndex), lf);
 		} else {
 			// do not know if there's metadata
-			insp = new MetadataInspector(metadata, lf, getPath(), new ContentPipe(sink, 0, lf));
+			insp = new MetadataInspector(metadata, lf, new ContentPipe(sink, 0, lf));
 		}
 		insp.checkCancelled();
 		super.content.iterate(fileRevisionIndex, fileRevisionIndex, true, insp);
 		try {
 			insp.checkFailed(); // XXX is there real need to throw IOException from ContentPipe?
-		} catch (HgDataStreamException ex) {
-			throw ex;
+		} catch (HgInvalidControlFileException ex) {
+			ex = ex.setFileName(getPath());
+			throw ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(fileRevisionIndex);
 		} catch (IOException ex) {
-			throw new HgDataStreamException(getPath(), ex).setRevisionIndex(fileRevisionIndex);
+			HgInvalidControlFileException e = new HgInvalidControlFileException("Revision content access failed", ex, null);
+			throw content.initWithIndexFile(e).setFileName(getPath()).setRevisionIndex(fileRevisionIndex);
 		} catch (HgException ex) {
 			// shall not happen, unless we changed ContentPipe or its subclass
-			throw new HgDataStreamException(getPath(), ex.getClass().getName(), ex);
+			HgInvalidControlFileException e = new HgInvalidControlFileException("Revision content access failed", ex, null);
+			throw content.initWithIndexFile(e).setFileName(getPath()).setRevisionIndex(fileRevisionIndex);
 		}
 	}
 	
@@ -460,11 +500,11 @@
 	}
 
 	/**
-	 * 
-	 * @return
-	 * @throws HgDataStreamException if attempt to access file metadata failed
+	 * Tells whether this file originates from another repository file
+	 * @return <code>true</code> if this file is a copy of another from the repository
+	 * @throws HgInvalidControlFileException if access to revlog or file metadata failed
 	 */
-	public boolean isCopy() throws HgDataStreamException {
+	public boolean isCopy() throws HgInvalidControlFileException {
 		if (metadata == null || !metadata.checked(0)) {
 			checkAndRecordMetadata(0);
 		}
@@ -478,17 +518,17 @@
 	 * Get name of the file this one was copied from.
 	 * 
 	 * @return name of the file origin
-	 * @throws HgDataStreamException if attempt to access file metadata failed
+	 * @throws HgInvalidControlFileException if access to revlog or file metadata failed
 	 * @throws UnsupportedOperationException if this file doesn't represent a copy ({@link #isCopy()} was false)
 	 */
-	public Path getCopySourceName() throws HgDataStreamException {
+	public Path getCopySourceName() throws HgInvalidControlFileException {
 		if (isCopy()) {
 			return Path.create(metadata.find(0, "copy"));
 		}
 		throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?)
 	}
 	
-	public Nodeid getCopySourceRevision() throws HgDataStreamException {
+	public Nodeid getCopySourceRevision() throws HgInvalidControlFileException {
 		if (isCopy()) {
 			return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid
 		}
@@ -504,9 +544,9 @@
 		return sb.toString();
 	}
 	
-	private void checkAndRecordMetadata(int localRev) throws HgDataStreamException {
+	private void checkAndRecordMetadata(int localRev) throws HgInvalidControlFileException {
 		// content() always initializes metadata.
-		// FIXME this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better.
+		// TODO [post-1.0] this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better.
 		// Alternatively, may parameterize MetadataContentPipe to do prepare only.
 		// For reference, when throwing CancelledException, hg status -A --rev 3:80 takes 70 ms
 		// however, if we just consume buffer instead (buffer.position(buffer.limit()), same command takes ~320ms
@@ -520,7 +560,10 @@
 		} catch (CancelledException ex) {
 			// it's ok, we did that
 		} catch (HgInvalidControlFileException ex) {
-			throw new HgDataStreamException(getPath(), ex);
+			throw ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(localRev);
+		} catch (HgException ex) {
+			// metadata comes from the content, hence initWithDataFile
+			throw content.initWithDataFile(new HgInvalidControlFileException(null, ex, null));
 		}
 	}
 
@@ -616,12 +659,10 @@
 	private static class MetadataInspector extends ErrorHandlingInspector implements RevlogStream.Inspector {
 		private final Metadata metadata;
 		private final RevlogStream.Inspector delegate;
-		private final Path fname; // need these only for error reporting
 		private final LogFacility log;
 
-		public MetadataInspector(Metadata _metadata, LogFacility logFacility, Path file, RevlogStream.Inspector chain) {
+		public MetadataInspector(Metadata _metadata, LogFacility logFacility, RevlogStream.Inspector chain) {
 			metadata = _metadata;
-			fname = file;
 			log = logFacility;
 			delegate = chain;
 			setCancelSupport(CancelSupport.Factory.get(chain));
@@ -648,12 +689,12 @@
 				}
 			} catch (IOException ex) {
 				recordFailure(ex);
-			} catch (HgDataStreamException ex) {
-				recordFailure(ex.setRevisionIndex(revisionNumber));
+			} catch (HgInvalidControlFileException ex) {
+				recordFailure(ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(revisionNumber));
 			}
 		}
 
-		private int parseMetadata(DataAccess data, final int daLength, ArrayList<MetadataEntry> _metadata) throws IOException, HgDataStreamException {
+		private int parseMetadata(DataAccess data, final int daLength, ArrayList<MetadataEntry> _metadata) throws IOException, HgInvalidControlFileException {
 			int lastEntryStart = 2;
 			int lastColon = -1;
 			// XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder,
@@ -705,7 +746,7 @@
 			// data.isEmpty is not reliable, renamed files of size==0 keep only metadata
 			if (!metadataIsComplete) {
 				// XXX perhaps, worth a testcase (empty file, renamed, read or ask ifCopy
-				throw new HgDataStreamException(fname, "Metadata is not closed properly", null);
+				throw new HgInvalidControlFileException("Metadata is not closed properly", null, null);
 			}
 			return lastEntryStart;
 		}
--- a/src/org/tmatesoft/hg/repo/HgInternals.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgInternals.java	Wed Mar 21 20:51:12 2012 +0100
@@ -144,17 +144,18 @@
 		return new FileWalker(repoRoot, pathSrc, workindDirScope);
 	}
 	
-	// expose othewise package-local information primarily to use in our own o.t.hg.core package
+	// expose otherwise package-local information primarily to use in our own o.t.hg.core package
 	public static SessionContext getContext(HgRepository repo) {
 		return repo.getContext();
 	}
 
 
 	// Convenient check of revision index for validity (not all negative values are wrong as long as we use negative constants)
-	public static boolean wrongRevisionIndex(int rev) {
-		return rev < 0 && rev != TIP && rev != WORKING_COPY && rev != BAD_REVISION; 
+	public static boolean wrongRevisionIndex(int rev) { // FIXME guess, usages shall throw HgInvalidRevision. \
+		// TODO Another method to check,throw and expand TIP at once
+		return rev < 0 && rev != TIP && rev != WORKING_COPY && rev != BAD_REVISION && rev != NO_REVISION; 
 	}
-
+	
 	// throws HgInvalidRevisionException or IllegalArgumentException if [start..end] range is not a subrange of [0..lastRevision]
 	public static void checkRevlogRange(int start, int end, int lastRevision) throws HgInvalidRevisionException {
 		if (start < 0 || start > lastRevision) {
--- a/src/org/tmatesoft/hg/repo/HgManifest.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgManifest.java	Wed Mar 21 20:51:12 2012 +0100
@@ -30,6 +30,7 @@
 import org.tmatesoft.hg.core.HgBadStateException;
 import org.tmatesoft.hg.core.HgException;
 import org.tmatesoft.hg.core.HgInvalidControlFileException;
+import org.tmatesoft.hg.core.HgInvalidRevisionException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.internal.DataAccess;
 import org.tmatesoft.hg.internal.DigestHelper;
@@ -54,7 +55,7 @@
 	private EncodingHelper encodingHelper;
 	
 	public enum Flags {
-		Exec, Link;
+		Exec, Link; // FIXME REVISIT consider REGULAR instead of null
 		
 		static Flags parse(String flags) {
 			if ("x".equalsIgnoreCase(flags)) {
@@ -131,9 +132,11 @@
 	 * @param start changelog (not manifest!) revision to begin with
 	 * @param end changelog (not manifest!) revision to stop, inclusive.
 	 * @param inspector manifest revision visitor, can't be <code>null</code>
+	 * @throws HgInvalidRevisionException if start or end specify non-existent revision index
+	 * @throws IllegalArgumentException if start or end is not a revision index
 	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
 	 */
-	public void walk(int start, int end, final Inspector inspector) throws /*FIXME HgInvalidRevisionException,*/ HgInvalidControlFileException {
+	public void walk(int start, int end, final Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		if (inspector == null) {
 			throw new IllegalArgumentException();
 		}
@@ -141,13 +144,13 @@
 		int manifestFirst, manifestLast, i = 0;
 		do {
 			manifestFirst = fromChangelog(csetFirst+i);
-			if (manifestFirst == -1) {
+			if (manifestFirst == BAD_REVISION) {
 				inspector.begin(BAD_REVISION, NULL, csetFirst+i);
 				inspector.end(BAD_REVISION);
 			}
 			i++;
-		} while (manifestFirst == -1 && csetFirst+i <= csetLast);
-		if (manifestFirst == -1) {
+		} while (manifestFirst == BAD_REVISION && csetFirst+i <= csetLast);
+		if (manifestFirst == BAD_REVISION) {
 			getRepo().getContext().getLog().info(getClass(), "None of changesets [%d..%d] have associated manifest revision", csetFirst, csetLast);
 			// we ran through all revisions in [start..end] and none of them had manifest.
 			// we reported that to inspector and proceeding is done now.
@@ -156,13 +159,13 @@
 		i = 0;
 		do {
 			manifestLast = fromChangelog(csetLast-i);
-			if (manifestLast == -1) {
+			if (manifestLast == BAD_REVISION) {
 				inspector.begin(BAD_REVISION, NULL, csetLast-i);
 				inspector.end(BAD_REVISION);
 			}
 			i++;
-		} while (manifestLast == -1 && csetLast-i >= csetFirst);
-		if (manifestLast == -1) {
+		} while (manifestLast == BAD_REVISION && csetLast-i >= csetFirst);
+		if (manifestLast == BAD_REVISION) {
 			// hmm, manifestFirst != -1 here, hence there's i from [csetFirst..csetLast] for which manifest entry exists, 
 			// and thus it's impossible to run into manifestLast == -1. Nevertheless, never hurts to check.
 			throw new HgBadStateException(String.format("Manifest %d-%d(!) for cset range [%d..%d] ", manifestFirst, manifestLast, csetFirst, csetLast));
@@ -185,8 +188,10 @@
 	 * 
 	 * @param inspector manifest revision visitor, can't be <code>null</code>
 	 * @param revisionIndexes local indexes of changesets to visit, non-<code>null</code>
+	 * @throws HgInvalidRevisionException if argument specifies non-existent revision index
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
 	 */
-	public void walk(final Inspector inspector, int... revisionIndexes) throws HgInvalidControlFileException{
+	public void walk(final Inspector inspector, int... revisionIndexes) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		if (inspector == null || revisionIndexes == null) {
 			throw new IllegalArgumentException();
 		}
@@ -196,10 +201,14 @@
 	
 	// 
 	/**
-	 * Tells manifest revision number that corresponds to the given changeset.
-	 * @return manifest revision index, or -1 if changeset has no associated manifest (cset records NULL nodeid for manifest) 
+	 * Tells manifest revision number that corresponds to the given changeset. May return {@link HgRepository#BAD_REVISION} 
+	 * if changeset has no associated manifest (cset records NULL nodeid for manifest).
+	 * @return manifest revision index, non-negative, or {@link HgRepository#BAD_REVISION}.
+	 * @throws HgInvalidRevisionException if method argument specifies non-existent revision index
+	 * @throws IllegalArgumentException if argument is not a revision index
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
 	 */
-	/*package-local*/ int fromChangelog(int changesetRevisionIndex) throws HgInvalidControlFileException {
+	/*package-local*/ int fromChangelog(int changesetRevisionIndex) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		if (HgInternals.wrongRevisionIndex(changesetRevisionIndex)) {
 			throw new IllegalArgumentException(String.valueOf(changesetRevisionIndex));
 		}
@@ -220,16 +229,18 @@
 	 * @param changelogRevisionIndex local changeset index 
 	 * @param file path to file in question
 	 * @return file revision or <code>null</code> if manifest at specified revision doesn't list such file
+	 * @throws HgInvalidRevisionException if method argument specifies non-existent revision index
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
 	 */
 	@Experimental(reason="Perhaps, HgDataFile shall own this method, or get a delegate?")
-	public Nodeid getFileRevision(int changelogRevisionIndex, final Path file) throws HgInvalidControlFileException{
+	public Nodeid getFileRevision(int changelogRevisionIndex, final Path file) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		return getFileRevisions(file, changelogRevisionIndex).get(changelogRevisionIndex);
 	}
 	
 	// XXX package-local, IntMap, and HgDataFile getFileRevisionAt(int... localChangelogRevisions)
 	@Experimental(reason="@see #getFileRevision")
-	public Map<Integer, Nodeid> getFileRevisions(final Path file, int... changelogRevisionIndexes) throws HgInvalidControlFileException{
-		// FIXME need tests
+	public Map<Integer, Nodeid> getFileRevisions(final Path file, int... changelogRevisionIndexes) throws HgInvalidRevisionException, HgInvalidControlFileException {
+		// TODO need tests
 		int[] manifestRevisionIndexes = toManifestRevisionIndexes(changelogRevisionIndexes, null);
 		final HashMap<Integer,Nodeid> rv = new HashMap<Integer, Nodeid>(changelogRevisionIndexes.length);
 		content.iterate(manifestRevisionIndexes, true, new RevlogStream.Inspector() {
@@ -269,14 +280,17 @@
 	/**
 	 * @param changelogRevisionIndexes non-null
 	 * @param inspector may be null if reporting of missing manifests is not needed
+	 * @throws HgInvalidRevisionException if arguments specify non-existent revision index
+	 * @throws IllegalArgumentException if any index argument is not a revision index
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
 	 */
-	private int[] toManifestRevisionIndexes(int[] changelogRevisionIndexes, Inspector inspector) throws HgInvalidControlFileException {
+	private int[] toManifestRevisionIndexes(int[] changelogRevisionIndexes, Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		int[] manifestRevs = new int[changelogRevisionIndexes.length];
 		boolean needsSort = false;
 		int j = 0;
 		for (int i = 0; i < changelogRevisionIndexes.length; i++) {
 			final int manifestRevisionIndex = fromChangelog(changelogRevisionIndexes[i]);
-			if (manifestRevisionIndex == -1) {
+			if (manifestRevisionIndex == BAD_REVISION) {
 				if (inspector != null) {
 					inspector.begin(BAD_REVISION, NULL, changelogRevisionIndexes[i]);
 					inspector.end(BAD_REVISION);
@@ -504,24 +518,32 @@
 	
 	private static class RevisionMapper implements RevlogStream.Inspector, Lifecycle {
 		
-		private final int changelogRevisions;
+		private final int changelogRevisionCount;
 		private int[] changelog2manifest;
 		private final HgRepository repo;
 
 		public RevisionMapper(HgRepository hgRepo) {
 			repo = hgRepo;
-			changelogRevisions = repo.getChangelog().getRevisionCount();
+			changelogRevisionCount = repo.getChangelog().getRevisionCount();
 		}
 
-		// respects TIP
-		public int at(int revisionNumber) {
-			if (revisionNumber == TIP) {
-				revisionNumber = changelogRevisions - 1;
+		/**
+		 * Get index of manifest revision that corresponds to specified changeset
+		 * @param changesetRevisionIndex non-negative index of changelog revision, or {@link HgRepository#TIP}
+		 * @return index of manifest revision, or {@link HgRepository#BAD_REVISION} if changeset doesn't reference a valid manifest
+		 * @throws HgInvalidRevisionException if method argument specifies non-existent revision index
+		 */
+		public int at(int changesetRevisionIndex) throws HgInvalidRevisionException {
+			if (changesetRevisionIndex == TIP) {
+				changesetRevisionIndex = changelogRevisionCount - 1;
+			}
+			if (changesetRevisionIndex >= changelogRevisionCount) {
+				throw new HgInvalidRevisionException(changesetRevisionIndex);
 			}
 			if (changelog2manifest != null) {
-				return changelog2manifest[revisionNumber];
+				return changelog2manifest[changesetRevisionIndex];
 			}
-			return revisionNumber;
+			return changesetRevisionIndex;
 		}
 
 		// XXX likely can be replaced with Revlog.RevisionInspector
@@ -530,12 +552,12 @@
 				// next assertion is not an error, rather assumption check, which is too development-related to be explicit exception - 
 				// I just wonder if there are manifests that have two entries pointing to single changeset. It seems unrealistic, though -
 				// changeset records one and only one manifest nodeid
-				assert changelog2manifest[linkRevision] == -1 : String.format("revision:%d, link:%d, already linked to revision:%d", revisionNumber, linkRevision, changelog2manifest[linkRevision]);
+				assert changelog2manifest[linkRevision] == BAD_REVISION : String.format("revision:%d, link:%d, already linked to revision:%d", revisionNumber, linkRevision, changelog2manifest[linkRevision]);
 				changelog2manifest[linkRevision] = revisionNumber;
 			} else {
 				if (revisionNumber != linkRevision) {
-					changelog2manifest = new int[changelogRevisions];
-					Arrays.fill(changelog2manifest, -1);
+					changelog2manifest = new int[changelogRevisionCount];
+					Arrays.fill(changelog2manifest, BAD_REVISION);
 					for (int i = 0; i < revisionNumber; changelog2manifest[i] = i, i++)
 						;
 					changelog2manifest[linkRevision] = revisionNumber;
@@ -544,14 +566,14 @@
 		}
 		
 		public void start(int count, Callback callback, Object token) {
-			if (count != changelogRevisions) {
-				assert count < changelogRevisions; // no idea what to do if manifest has more revisions than changelog
+			if (count != changelogRevisionCount) {
+				assert count < changelogRevisionCount; // no idea what to do if manifest has more revisions than changelog
 				// the way how manifest may contain more revisions than changelog, as I can imagine, is a result of  
 				// some kind of an import tool (e.g. from SVN or CVS), that creates manifest and changelog independently.
 				// Note, it's pure guess, I didn't see such repository yet (although the way manifest revisions
 				// in cpython repo are numbered makes me think aforementioned way) 
-				changelog2manifest = new int[changelogRevisions];
-				Arrays.fill(changelog2manifest, -1);
+				changelog2manifest = new int[changelogRevisionCount];
+				Arrays.fill(changelog2manifest, BAD_REVISION);
 			}
 		}
 
@@ -562,7 +584,7 @@
 			// I assume there'd be not too many revisions we don't know manifest of
 			ArrayList<Integer> undefinedChangelogRevision = new ArrayList<Integer>();
 			for (int i = 0; i < changelog2manifest.length; i++) {
-				if (changelog2manifest[i] == -1) {
+				if (changelog2manifest[i] == BAD_REVISION) {
 					undefinedChangelogRevision.add(i);
 				}
 			}
--- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Wed Mar 21 20:51:12 2012 +0100
@@ -234,12 +234,12 @@
 						assert currRange == null;
 						assert currRangeList == null;
 						if (!rangeItr.hasNext()) {
-							throw new HgBadStateException();
+							throw new HgBadStateException("Internal error");
 						}
 						rv.put(rangeItr.next(), Collections.<Nodeid>emptyList());
 					} else {
 						if (currRange == null || currRangeList == null) {
-							throw new HgBadStateException();
+							throw new HgBadStateException("Internal error");
 						}
 						// indicate next range value is needed
 						currRange = null;
@@ -250,7 +250,7 @@
 					possiblyEmptyNextLine = false;
 					if (currRange == null) {
 						if (!rangeItr.hasNext()) {
-							throw new HgBadStateException();
+							throw new HgBadStateException("Internal error");
 						}
 						currRange = rangeItr.next();
 						currRangeList = new LinkedList<Nodeid>();
--- a/src/org/tmatesoft/hg/repo/HgRepository.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgRepository.java	Wed Mar 21 20:51:12 2012 +0100
@@ -25,8 +25,9 @@
 import java.util.HashMap;
 import java.util.List;
 
-import org.tmatesoft.hg.core.HgDataStreamException;
+import org.tmatesoft.hg.core.HgException;
 import org.tmatesoft.hg.core.HgInvalidControlFileException;
+import org.tmatesoft.hg.core.HgInvalidRevisionException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.SessionContext;
 import org.tmatesoft.hg.internal.ByteArrayChannel;
@@ -54,11 +55,35 @@
  */
 public final class HgRepository {
 
-	// if new constants added, consider fixing HgInternals#wrongRevisionIndex
-	public static final int TIP = -3;
-	public static final int BAD_REVISION = Integer.MIN_VALUE;
-	public static final int WORKING_COPY = -2;
+	// IMPORTANT: if new constants added, consider fixing HgInternals#wrongRevisionIndex and HgInvalidRevisionException#getMessage
+
+	/**
+	 * Revision index constant to indicate most recent revision
+	 */
+	public static final int TIP = -3; // XXX TIP_REVISION?
+
+	/**
+	 * Revision index constant to indicate invalid revision index value. 
+	 * Primary use is default/uninitialized values where user input is expected and as return value where 
+	 * an exception (e.g. {@link HgInvalidRevisionException}) is not desired
+	 */
+	public static final int BAD_REVISION = Integer.MIN_VALUE; // XXX INVALID_REVISION?
+
+	/**
+	 * Revision index constant to indicate working copy
+	 */
+	public static final int WORKING_COPY = -2; // XXX WORKING_COPY_REVISION?
 	
+	/**
+	 * Constant ({@value #NO_REVISION}) to indicate revision absence (e.g. missing parent in from {@link HgChangelog#parents(int, int[], byte[], byte[])} call) 
+	 * or a fictitious revision of an empty repository, to use as an argument (contrary to {@link #BAD_REVISION})
+	 * e.g in a status operation to visit changes from the very beginning of a repository. 
+	 */
+	public static final int NO_REVISION = -1;
+	
+	/**
+	 * Name of the primary branch, "default".
+	 */
 	public static final String DEFAULT_BRANCH_NAME = "default";
 
 	// temp aux marker method
@@ -165,7 +190,7 @@
 					} catch (CancelledException ex) {
 						 // IGNORE, can't happen, we did not configure cancellation
 						getContext().getLog().debug(getClass(), ex, null);
-					} catch (HgDataStreamException ex) {
+					} catch (HgException ex) {
 						getContext().getLog().error(getClass(), ex, null);
 						// FIXME need to react
 					} catch (IOException ex) {
--- a/src/org/tmatesoft/hg/repo/HgStatusCollector.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgStatusCollector.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,8 +16,7 @@
  */
 package org.tmatesoft.hg.repo;
 
-import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
-import static org.tmatesoft.hg.repo.HgRepository.TIP;
+import static org.tmatesoft.hg.repo.HgRepository.*;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -28,9 +27,9 @@
 import java.util.TreeSet;
 
 import org.tmatesoft.hg.core.HgBadStateException;
-import org.tmatesoft.hg.core.HgDataStreamException;
 import org.tmatesoft.hg.core.HgException;
 import org.tmatesoft.hg.core.HgInvalidControlFileException;
+import org.tmatesoft.hg.core.HgInvalidRevisionException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.internal.IntMap;
 import org.tmatesoft.hg.internal.ManifestRevision;
@@ -78,7 +77,7 @@
 	private ManifestRevision get(int rev) throws HgInvalidControlFileException {
 		ManifestRevision i = cache.get(rev);
 		if (i == null) {
-			if (rev == -1) {
+			if (rev == NO_REVISION) {
 				return emptyFakeState;
 			}
 			ensureCacheSize();
@@ -90,7 +89,7 @@
 	}
 
 	private boolean cached(int revision) {
-		return cache.containsKey(revision) || revision == -1;
+		return cache.containsKey(revision) || revision == NO_REVISION;
 	}
 	
 	private void ensureCacheSize() {
@@ -118,7 +117,7 @@
 
 			public boolean begin(int manifestRevision, Nodeid nid, int changelogRevision) {
 				assert delegate == null;
-				if (cache.containsKey(changelogRevision)) { // don't need to check emptyFakeState hit as revision never -1 here
+				if (cache.containsKey(changelogRevision)) { // don't need to check emptyFakeState hit as revision never NO_REVISION here
 					cacheHit = true;
 				} else {
 					cache.put(changelogRevision, delegate = new ManifestRevision(cacheNodes, cacheFilenames));
@@ -154,11 +153,17 @@
 	
 	/*package-local*/ static ManifestRevision createEmptyManifestRevision() {
 		ManifestRevision fakeEmptyRev = new ManifestRevision(null, null);
-		fakeEmptyRev.begin(-1, null, -1);
-		fakeEmptyRev.end(-1);
+		fakeEmptyRev.begin(NO_REVISION, null, NO_REVISION);
+		fakeEmptyRev.end(NO_REVISION);
 		return fakeEmptyRev;
 	}
 	
+	/**
+	 * Access specific manifest revision
+	 * @param rev 
+	 * @return
+	 * @throws HgInvalidControlFileException
+	 */
 	/*package-local*/ ManifestRevision raw(int rev) throws HgInvalidControlFileException {
 		return get(rev);
 	}
@@ -185,30 +190,57 @@
 		scope = scopeMatcher == null ? new Path.Matcher.Any() : scopeMatcher;
 	}
 	
-	// hg status --change <rev>
-	public void change(int rev, HgStatusInspector inspector) throws /*FIXME HInvalidRevisionException,*/ HgInvalidControlFileException {
-		int[] parents = new int[2];
-		repo.getChangelog().parents(rev, parents, null, null);
-		walk(parents[0], rev, inspector);
+	/**
+	 * 'hg status --change REV' command counterpart.
+	 * 
+	 * @throws HgInvalidRevisionException if argument specifies non-existent revision index
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 */
+	public void change(int revisionIndex, HgStatusInspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException {
+		int p;
+		if (revisionIndex == 0) {
+			p = NO_REVISION;
+		} else {
+			int[] parents = new int[2];
+			repo.getChangelog().parents(revisionIndex, parents, null, null);
+			// #parents call above is responsible for NO_REVISION
+			p = parents[0]; // hg --change alsways uses first parent, despite the fact there might be valid (-1, 18) pair of parents
+		}
+		walk(p, revisionIndex, inspector);
 	}
 	
-	// rev1 and rev2 are changelog revision numbers, argument order matters.
-	// Either rev1 or rev2 may be -1 to indicate comparison to empty repository (XXX this is due to use of 
-	// parents in #change(), I believe. Perhaps, need a constant for this? Otherwise this hidden knowledge gets
-	// exposed to e.g. Record
-	public void walk(int rev1, int rev2, HgStatusInspector inspector) throws /*FIXME HInvalidRevisionException,*/ HgInvalidControlFileException {
+	/**
+	 * Parameters <b>rev1</b> and <b>rev2</b> are changelog revision indexes, shall not be the same. Argument order matters.
+	 * Either rev1 or rev2 may be {@link HgRepository#NO_REVISION} to indicate comparison to empty repository
+	 * 
+	 * FIXME cancellation (at least exception)?
+	 * 
+	 * @param rev1 <em>from</em> changeset index, non-negative or {@link HgRepository#TIP}
+	 * @param rev2 <em>to</em> changeset index, non-negative or {@link HgRepository#TIP}
+	 * @param inspector callback for status information
+	 * @throws HgInvalidRevisionException if any argument specifies non-existent revision index
+	 * @throws IllegalArgumentException inspector other incorrect argument values
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 */
+	public void walk(int rev1, int rev2, HgStatusInspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		if (rev1 == rev2) {
 			throw new IllegalArgumentException();
 		}
 		if (inspector == null) {
 			throw new IllegalArgumentException();
 		}
-		final int lastManifestRevision = repo.getChangelog().getLastRevision();
+		final int lastChangelogRevision = repo.getChangelog().getLastRevision();
 		if (rev1 == TIP) {
-			rev1 = lastManifestRevision;
+			rev1 = lastChangelogRevision;
 		}
 		if (rev2 == TIP) {
-			rev2 = lastManifestRevision; 
+			rev2 = lastChangelogRevision; 
+		}
+		if (rev1 != NO_REVISION && (HgInternals.wrongRevisionIndex(rev1) || rev1 == WORKING_COPY || rev1 == BAD_REVISION || rev1 > lastChangelogRevision)) {
+			throw new HgInvalidRevisionException(rev1);
+		}
+		if (rev2 != NO_REVISION && (HgInternals.wrongRevisionIndex(rev2) || rev2 == WORKING_COPY || rev2 == BAD_REVISION || rev2 > lastChangelogRevision)) {
+			throw new HgInvalidRevisionException(rev2);
 		}
 		if (inspector instanceof Record) {
 			((Record) inspector).init(rev1, rev2, this);
@@ -234,12 +266,12 @@
 			// which going to be read anyway
 			if (need1) {
 				minRev = rev1;
-				maxRev = rev1 < lastManifestRevision-5 ? rev1+5 : lastManifestRevision;
+				maxRev = rev1 < lastChangelogRevision-5 ? rev1+5 : lastChangelogRevision;
 				initCacheRange(minRev, maxRev);
 			}
 			if (need2) {
 				minRev = rev2;
-				maxRev = rev2 < lastManifestRevision-5 ? rev2+5 : lastManifestRevision;
+				maxRev = rev2 < lastChangelogRevision-5 ? rev2+5 : lastChangelogRevision;
 				initCacheRange(minRev, maxRev);
 			}
 		}
@@ -284,17 +316,26 @@
 		}
 	}
 	
-	public Record status(int rev1, int rev2) throws /*FIXME HInvalidRevisionException,*/ HgInvalidControlFileException {
+	/**
+	 * Collects status between two revisions, changes from <b>rev1</b> up to <b>rev2</b>.
+	 * 
+	 * @param rev1 <em>from</em> changeset index 
+	 * @param rev2 <em>to</em> changeset index
+	 * @return information object that describes change between the revisions
+	 * @throws HgInvalidRevisionException if any argument specifies non-existent revision index
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 */
+	public Record status(int rev1, int rev2) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		Record rv = new Record();
 		walk(rev1, rev2, rv);
 		return rv;
 	}
 	
-	/*package-local*/static Path getOriginIfCopy(HgRepository hgRepo, Path fname, Collection<Path> originals, int originalChangelogRevision) throws HgDataStreamException, HgInvalidControlFileException {
+	/*package-local*/static Path getOriginIfCopy(HgRepository hgRepo, Path fname, Collection<Path> originals, int originalChangelogRevision) throws HgException {
 		HgDataFile df = hgRepo.getFileNode(fname);
 		if (!df.exists()) {
 			String msg = String.format("Didn't find file '%s' in the repo. Perhaps, bad storage name conversion?", fname);
-			throw new HgDataStreamException(fname, msg, null).setRevisionIndex(originalChangelogRevision);
+			throw new HgException(msg).setFileName(fname).setRevisionIndex(originalChangelogRevision);
 		}
 		while (df.isCopy()) {
 			Path original = df.getCopySourceName();
--- a/src/org/tmatesoft/hg/repo/HgSubrepoLocation.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgSubrepoLocation.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -88,7 +88,7 @@
 
 	public HgRepository getRepo() throws HgInvalidFileException {
 		if (kind != Kind.Hg) {
-			throw new HgBadStateException();
+			throw new HgBadStateException(String.format("Unsupported subrepository %s", kind));
 		}
 		return new HgLookup().detect(new File(owner.getWorkingDir(), source));
 	}
--- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Wed Mar 21 20:51:12 2012 +0100
@@ -37,6 +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.Internals;
 import org.tmatesoft.hg.internal.ManifestRevision;
 import org.tmatesoft.hg.internal.PathScope;
 import org.tmatesoft.hg.internal.Preview;
@@ -137,7 +138,7 @@
 	private void initDirstateParentManifest() throws HgInvalidControlFileException {
 		Nodeid dirstateParent = getDirstateImpl().parents().first();
 		if (dirstateParent.isNull()) {
-			dirstateParentManifest = baseRevisionCollector != null ? baseRevisionCollector.raw(-1) : HgStatusCollector.createEmptyManifestRevision();
+			dirstateParentManifest = baseRevisionCollector != null ? baseRevisionCollector.raw(NO_REVISION) : HgStatusCollector.createEmptyManifestRevision();
 		} else {
 			int changeloRevIndex = repo.getChangelog().getRevisionIndex(dirstateParent);
 			dirstateParentManifest = getManifest(changeloRevIndex);
@@ -287,27 +288,37 @@
 			// either clean or modified
 			final boolean timestampEqual = f.lastModified() == r.modificationTime(), sizeEqual = r.size() == f.length();
 			if (timestampEqual && sizeEqual) {
-				inspector.clean(fname);
+				// if flags change (chmod -x), timestamp does not change
+				if (checkFlagsEqual(f, r.mode())) {
+					inspector.clean(fname);
+				} else {
+					inspector.modified(fname); // flags are not the same
+				}
 			} else if (!sizeEqual && r.size() >= 0) {
 				inspector.modified(fname);
 			} else {
+				// size is the same or unknown, and, perhaps, different timestamp
+				// check actual content to avoid false modified files
 				try {
-					// size is the same or unknown, and, perhaps, different timestamp
-					// check actual content to avoid false modified files
-					HgDataFile df = repo.getFileNode(fname);
-					if (!df.exists()) {
-						String msg = String.format("File %s known as normal in dirstate (%d, %d), doesn't exist at %s", fname, r.modificationTime(), r.size(), repo.getStoragePath(df));
-						throw new HgInvalidFileException(msg, null).setFileName(fname);
-					}
-					Nodeid rev = getDirstateParentManifest().nodeid(fname);
-					// rev might be null here if fname comes to dirstate as a result of a merge operation
-					// where one of the parents (first parent) had no fname file, but second parent had.
-					// E.g. fork revision 3, revision 4 gets .hgtags, few modifications and merge(3,12)
-					// see Issue 14 for details
-					if (rev == null || !areTheSame(f, df, rev)) {
-						inspector.modified(df.getPath());
+					if (!checkFlagsEqual(f, r.mode())) {
+						// flags modified, no need to do expensive content check
+						inspector.modified(fname);
 					} else {
-						inspector.clean(df.getPath());
+						HgDataFile df = repo.getFileNode(fname);
+						if (!df.exists()) {
+							String msg = String.format("File %s known as normal in dirstate (%d, %d), doesn't exist at %s", fname, r.modificationTime(), r.size(), repo.getStoragePath(df));
+							throw new HgInvalidFileException(msg, null).setFileName(fname);
+						}
+						Nodeid rev = getDirstateParentManifest().nodeid(fname);
+						// rev might be null here if fname comes to dirstate as a result of a merge operation
+						// where one of the parents (first parent) had no fname file, but second parent had.
+						// E.g. fork revision 3, revision 4 gets .hgtags, few modifications and merge(3,12)
+						// see Issue 14 for details
+						if (rev == null || !areTheSame(f, df, rev)) {
+							inspector.modified(df.getPath());
+						} else {
+							inspector.clean(df.getPath());
+						}
 					}
 				} catch (HgException ex) {
 					repo.getContext().getLog().warn(getClass(), ex, null);
@@ -374,7 +385,7 @@
 				} else if (!sizeEqual && r.size() >= 0) {
 					inspector.modified(fname);
 					handled = true;
-				} else if (!todoCheckFlagsEqual(f, flags)) {
+				} else if (!checkFlagsEqual(f, flags)) {
 					// seems like flags have changed, no reason to check content further
 					inspector.modified(fname);
 					handled = true;
@@ -516,9 +527,39 @@
 		}
 	}
 
-	private static boolean todoCheckFlagsEqual(FileInfo f, HgManifest.Flags originalManifestFlags) {
-		// FIXME implement
-		return true;
+	/**
+	 * @return <code>true</code> if flags are the same
+	 */
+	private boolean checkFlagsEqual(FileInfo f, HgManifest.Flags originalManifestFlags) {
+		boolean same = true;
+		if (repoWalker.supportsLinkFlag()) {
+			if (originalManifestFlags == HgManifest.Flags.Link) {
+				return f.isSymlink();
+			}
+			// original flag is not link, hence flags are the same if file is not link, too.
+			same = !f.isSymlink();
+		} // otherwise treat flags the same
+		if (repoWalker.supportsExecFlag()) {
+			if (originalManifestFlags == HgManifest.Flags.Exec) {
+				return f.isExecutable();
+			}
+			// original flag has no executable attribute, hence file shall not be executable, too
+			same = same || !f.isExecutable();
+		}
+		return same;
+	}
+	
+	private boolean checkFlagsEqual(FileInfo f, int dirstateFileMode) {
+		// source/include/linux/stat.h
+		final int S_IFLNK = 0120000, S_IXUSR = 00100;
+		// TODO post-1.0 HgManifest.Flags.parse(int)
+		if ((dirstateFileMode & S_IFLNK) == S_IFLNK) {
+			return checkFlagsEqual(f, HgManifest.Flags.Link);
+		}
+		if ((dirstateFileMode & S_IXUSR) == S_IXUSR) {
+			return checkFlagsEqual(f, HgManifest.Flags.Exec);
+		}
+		return checkFlagsEqual(f, null); // no flags
 	}
 
 	/**
@@ -580,16 +621,19 @@
 		private final Path[] paths;
 		private int index;
 		private RegularFileInfo nextFile;
+		private final boolean execCap, linkCap;
 
 		public FileListIterator(File startDir, Path... files) {
 			dir = startDir;
 			paths = files;
 			reset();
+			execCap = Internals.checkSupportsExecutables(startDir);
+			linkCap = Internals.checkSupportsSymlinks(startDir);
 		}
 
 		public void reset() {
 			index = -1;
-			nextFile = new RegularFileInfo();
+			nextFile = new RegularFileInfo(execCap, linkCap);
 		}
 
 		public boolean hasNext() {
@@ -620,6 +664,16 @@
 			}
 			return false;
 		}
+		
+		public boolean supportsExecFlag() {
+			// TODO Auto-generated method stub
+			return false;
+		}
+		
+		public boolean supportsLinkFlag() {
+			// TODO Auto-generated method stub
+			return false;
+		}
 	}
 	
 	private static class FileIteratorFilter implements FileIterator {
@@ -670,5 +724,13 @@
 		public boolean inScope(Path file) {
 			return filter.accept(file);
 		}
+		
+		public boolean supportsExecFlag() {
+			return walker.supportsExecFlag();
+		}
+		
+		public boolean supportsLinkFlag() {
+			return walker.supportsLinkFlag();
+		}
 	}
 }
--- a/src/org/tmatesoft/hg/repo/Revlog.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/Revlog.java	Wed Mar 21 20:51:12 2012 +0100
@@ -17,6 +17,7 @@
 package org.tmatesoft.hg.repo;
 
 import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
+import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import java.io.IOException;
@@ -177,35 +178,65 @@
 	}
 
 	/**
-	 * Access to revision data as is (decompressed, but otherwise unprocessed, i.e. not parsed for e.g. changeset or manifest entries) 
-	 * @param nodeid
+	 * Access to revision data as is, equivalent to <code>rawContent(getRevisionIndex(nodeid), sink)</code>
+	 * 
+	 * @param nodeid revision to retrieve
+	 * @param sink data destination
+	 * 
+	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws CancelledException if content retrieval operation was cancelled
+	 * 
+	 * @see #rawContent(int, ByteChannel)
 	 */
-	protected void rawContent(Nodeid nodeid, ByteChannel sink) throws HgException, IOException, CancelledException, HgInvalidRevisionException {
+	protected void rawContent(Nodeid nodeid, ByteChannel sink) throws HgInvalidControlFileException, CancelledException, HgInvalidRevisionException {
 		rawContent(getRevisionIndex(nodeid), sink);
 	}
 	
 	/**
-	 * @param fileRevisionIndex - index of this file change (not a changelog revision index), non-negative. From predefined constants, only {@link HgRepository#TIP} makes sense. 
-	 * FIXME is it necessary to have IOException along with HgException here?
+	 * Access to revision data as is (decompressed, but otherwise unprocessed, i.e. not parsed for e.g. changeset or manifest entries).
+	 *  
+	 * @param fileRevisionIndex index of this revlog change (not a changelog revision index), non-negative. From predefined constants, only {@link HgRepository#TIP} makes sense.
+	 * @param sink data destination
+	 * 
+	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws CancelledException if content retrieval operation was cancelled
 	 */
-	protected void rawContent(int fileRevisionIndex, ByteChannel sink) throws HgException, IOException, CancelledException, HgInvalidRevisionException {
+	protected void rawContent(int fileRevisionIndex, ByteChannel sink) throws HgInvalidControlFileException, CancelledException, HgInvalidRevisionException {
 		if (sink == null) {
 			throw new IllegalArgumentException();
 		}
-		ContentPipe insp = new ContentPipe(sink, 0, repo.getContext().getLog());
-		insp.checkCancelled();
-		content.iterate(fileRevisionIndex, fileRevisionIndex, true, insp);
-		insp.checkFailed();
+		try {
+			ContentPipe insp = new ContentPipe(sink, 0, repo.getContext().getLog());
+			insp.checkCancelled();
+			content.iterate(fileRevisionIndex, fileRevisionIndex, true, insp);
+			insp.checkFailed();
+		} catch (IOException ex) {
+			HgInvalidControlFileException e = new HgInvalidControlFileException(String.format("Access to revision %d content failed", fileRevisionIndex), ex, null);
+			e.setRevisionIndex(fileRevisionIndex);
+			// FIXME e.setFileName(content.getIndexFile() or this.getHumanFriendlyPath()) - shall decide whether 
+			// protected abstract getPath() with impl in HgDataFile, HgManifest and HgChangelog or path is data of either Revlog or RevlogStream
+			// Do the same (add file name) below
+			throw e;
+		} catch (HgInvalidControlFileException ex) {
+			throw ex;
+		} catch (HgException ex) {
+			HgInvalidControlFileException e = new HgInvalidControlFileException(ex.getClass().getSimpleName(), ex, null);
+			e.setRevisionIndex(fileRevisionIndex);
+			throw e;
+		}
 	}
 
 	/**
-	 * XXX perhaps, return value Nodeid[2] and boolean needNodeids is better (and higher level) API for this query?
+	 * Fills supplied arguments with information about revision parents.
 	 * 
 	 * @param revision - revision to query parents, or {@link HgRepository#TIP}
-	 * @param parentRevisions - int[2] to get local revision numbers of parents (e.g. {6, -1})
+	 * @param parentRevisions - int[2] to get local revision numbers of parents (e.g. {6, -1}), {@link HgRepository#NO_REVISION} indicates parent not set
 	 * @param parent1 - byte[20] or null, if parent's nodeid is not needed
 	 * @param parent2 - byte[20] or null, if second parent's nodeid is not needed
 	 * @throws HgInvalidRevisionException
+	 * @throws HgInvalidControlFileException FIXME
 	 * @throws IllegalArgumentException
 	 */
 	public void parents(int revision, int[] parentRevisions, byte[] parent1, byte[] parent2) throws HgInvalidRevisionException, HgInvalidControlFileException {
@@ -236,10 +267,11 @@
 		};
 		ParentCollector pc = new ParentCollector();
 		content.iterate(revision, revision, false, pc);
-		parentRevisions[0] = pc.p1;
-		parentRevisions[1] = pc.p2;
+		// although next code looks odd (NO_REVISION *is* -1), it's safer to be explicit
+		parentRevisions[0] = pc.p1 == -1 ? NO_REVISION : pc.p1;
+		parentRevisions[1] = pc.p2 == -1 ? NO_REVISION : pc.p2;
 		if (parent1 != null) {
-			if (parentRevisions[0] == -1) {
+			if (parentRevisions[0] == NO_REVISION) {
 				Arrays.fill(parent1, 0, 20, (byte) 0);
 			} else {
 				content.iterate(parentRevisions[0], parentRevisions[0], false, pc);
@@ -247,7 +279,7 @@
 			}
 		}
 		if (parent2 != null) {
-			if (parentRevisions[1] == -1) {
+			if (parentRevisions[1] == NO_REVISION) {
 				Arrays.fill(parent2, 0, 20, (byte) 0);
 			} else {
 				content.iterate(parentRevisions[1], parentRevisions[1], false, pc);
@@ -367,7 +399,7 @@
 		
 		private void assertSortedIndex(int x) {
 			if (x < 0) {
-				throw new HgBadStateException();
+				throw new HgBadStateException(String.format("Bad index", x));
 			}
 		}
 		
@@ -585,6 +617,7 @@
 			failure = ex;
 		}
 
+		// TODO consider if IOException in addition to HgException is of any real utility
 		public void checkFailed() throws HgException, IOException, CancelledException {
 			if (failure == null) {
 				return;
--- a/src/org/tmatesoft/hg/repo/package.html	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/package.html	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,6 @@
 <html>
-<boody>
-Low-level API operations
-</bidy>
+<body>
+<h2>Low-level API</h2>
+<p>Close perspective of Mercurial repository from Java standpoint. Unlike {@link org.tmatesoft.hg.core}, classes in this package focus on repository concepts rather than tasks against the repository</p>
+</body>
 </html>
\ No newline at end of file
--- a/src/org/tmatesoft/hg/util/FileInfo.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/util/FileInfo.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -47,4 +47,16 @@
 	 * @return file reader object, never <code>null</code>
 	 */
 	ReadableByteChannel newInputChannel();
+
+	/**
+	 * This method is invoked only if source FileIterator tells <code>true</code> for {@link FileIterator#supportsExecFlag()}
+	 * @return <code>true</code> if this object describes an executable file
+	 */
+	boolean isExecutable();
+
+	/**
+	 * This method is be invoked only if source FileIterator tells <code>true</code> for {@link FileIterator#supportsLinkFlag()}.
+	 * @return <code>true</code> if this file object represents a symbolic link
+	 */
+	boolean isSymlink();
 }
--- a/src/org/tmatesoft/hg/util/FileIterator.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/util/FileIterator.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -63,5 +63,20 @@
 	 * @return <code>true</code> if this {@link FileIterator} is responsible for (interested in) specified repository-local path 
 	 */
 	@Experimental(reason="Perhaps, shall not be part of FileIterator, but rather separate Path.Matcher. Approaches in regular StatusCollector (doesn't use FI, but supports scope) and WC collector to look similar, and for HgStatusCommand to use single approach to set the scope")
-	boolean inScope(Path file);
+	boolean inScope(Path file); // PathMatcher scope()
+
+	/**
+	 * Tells whether caller shall be aware of distinction between executable and non-executable files coming from this iterator.
+	 * Note, these days Mercurial (as of 2.1) doesn't recognize Windows .exe files as executable (nor it treats any Windows filesystem as exec-capable) 
+	 * @return <code>true</code> if file descriptors are capable to provide executable flag
+	 */
+	boolean supportsExecFlag();
+
+	/**
+	 * POSIX file systems allow symbolic links to files, and these links are handled in a special way with Mercurial, i.e. it tracks value of 
+	 * the link, not its actual target.
+	 * Note, these days Mercurial (as of 2.1) doesn't support Windows Vista/7 symlinks.
+	 * @return <code>true</code> if file descriptors are capable to tell symlink files from regular ones. 
+	 */
+	boolean supportsLinkFlag();
 }
--- a/src/org/tmatesoft/hg/util/FileWalker.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/util/FileWalker.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,10 @@
 import java.util.LinkedList;
 import java.util.NoSuchElementException;
 
+import org.tmatesoft.hg.internal.Internals;
+
 /**
+ * Implementation of {@link FileIterator} using regular {@link java.io.File}
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
@@ -32,6 +35,7 @@
 	private final LinkedList<File> dirQueue;
 	private final LinkedList<File> fileQueue;
 	private final Path.Matcher scope;
+	private final boolean execCap, linkCap;
 	private RegularFileInfo nextFile;
 	private Path nextPath;
 
@@ -53,6 +57,8 @@
 		dirQueue = new LinkedList<File>();
 		fileQueue = new LinkedList<File>();
 		scope = scopeMatcher;
+		execCap = Internals.checkSupportsExecutables(startDir);
+		linkCap = Internals.checkSupportsSymlinks(startDir);
 		reset();
 	}
 
@@ -60,7 +66,7 @@
 		fileQueue.clear();
 		dirQueue.clear();
 		dirQueue.add(startDir);
-		nextFile = new RegularFileInfo();
+		nextFile = new RegularFileInfo(supportsExecFlag(), supportsLinkFlag());
 		nextPath = null;
 	}
 	
@@ -90,6 +96,14 @@
 		return scope == null ? true : scope.accept(file); 
 	}
 	
+	public boolean supportsExecFlag() {
+		return execCap;
+	}
+	
+	public boolean supportsLinkFlag() {
+		return linkCap;
+	}
+		
 	// returns non-null
 	private File[] listFiles(File f) {
 		// in case we need to solve os-related file issues (mac with some encodings?)
--- a/src/org/tmatesoft/hg/util/RegularFileInfo.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/src/org/tmatesoft/hg/util/RegularFileInfo.java	Wed Mar 21 20:51:12 2012 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011 TMate Software Ltd
+ * Copyright (c) 2011-2012 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -21,6 +21,7 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
 import java.nio.channels.ReadableByteChannel;
 
 import org.tmatesoft.hg.internal.StreamLogFacility;
@@ -31,48 +32,111 @@
  * @author TMate Software Ltd.
  */
 public class RegularFileInfo implements FileInfo {
+	private final boolean supportsExec, supportsLink;
+	private final RegularFileStats fileFlagsHelper; // null if both supportsLink and supportExec are false
 	private File file;
 	
 	public RegularFileInfo() {
+		this(false, false);
+	}
+	public RegularFileInfo(boolean supportExecFlag, boolean supportSymlink) {
+		supportsLink = supportSymlink;
+		supportsExec = supportExecFlag;
+		if (supportSymlink || supportExecFlag) {
+			fileFlagsHelper = new RegularFileStats();
+		} else  {
+			fileFlagsHelper = null;
+		}
 	}
 	
 	public void init(File f) {
 		file = f;
+		if (fileFlagsHelper != null) {
+			fileFlagsHelper.init(file);
+		}
 	}
 	
 	public boolean exists() {
-		return file.canRead() && file.isFile();
+		// java.io.File for symlinks without proper target says it doesn't exist.
+		// since we found this symlink in directory listing, it's safe to say it exists just based on the fact it's link
+		return isSymlink() || (file.canRead() && file.isFile());
 	}
 
 	public int lastModified() {
+		// TODO post-1.0 for symlinks, this returns incorrect mtime of the target file, not that of link itself
+		// Besides, timestame if link points to non-existing file is 0.
+		// However, it result only in slowdown in WCStatusCollector, as it need to perform additional content check
 		return (int) (file.lastModified() / 1000);
 	}
 
 	public long length() {
+		if (isSymlink()) {
+			return getLinkTargetBytes().length;
+		}
 		return file.length();
 	}
 
 	public ReadableByteChannel newInputChannel() {
 		try {
-			return new FileInputStream(file).getChannel();
+			if (isSymlink()) {
+				return new ByteArrayReadableChannel(getLinkTargetBytes());
+			} else {
+				return new FileInputStream(file).getChannel();
+			}
 		} catch (FileNotFoundException ex) {
 			StreamLogFacility.newDefault().debug(getClass(), ex, null);
 			// shall not happen, provided this class is used correctly
-			return new ReadableByteChannel() {
-				
-				public boolean isOpen() {
-					return true;
-				}
-				
-				public void close() throws IOException {
-				}
-				
-				public int read(ByteBuffer dst) throws IOException {
-					// EOF right away
-					return -1;
-				}
-			};
+			return new ByteArrayReadableChannel(null);
 		}
 	}
 
+	public boolean isExecutable() {
+		return supportsExec && fileFlagsHelper.isExecutable();
+	}
+	
+	public boolean isSymlink() {
+		return supportsLink && fileFlagsHelper.isSymlink();
+	}
+	
+	private byte[] getLinkTargetBytes() {
+		assert isSymlink();
+		// no idea what encoding Mercurial uses for link targets, assume platform native is ok
+		return fileFlagsHelper.getSymlinkTarget().getBytes();
+	}
+
+
+	private static class ByteArrayReadableChannel implements ReadableByteChannel {
+		private final byte[] data;
+		private boolean closed = false; // initially open
+		private int firstAvailIndex = 0;
+		
+		ByteArrayReadableChannel(byte[] dataToStream) {
+			data = dataToStream;
+		}
+
+		public boolean isOpen() {
+			return !closed;
+		}
+
+		public void close() throws IOException {
+			closed = true;
+		}
+
+		public int read(ByteBuffer dst) throws IOException {
+			if (closed) {
+				throw new ClosedChannelException();
+			}
+			int remainingBytes = data.length - firstAvailIndex;
+			if (data == null || remainingBytes == 0) {
+				// EOF right away
+				return -1;
+			}
+			int x = Math.min(dst.remaining(), remainingBytes);
+			for (int i = firstAvailIndex, lim = firstAvailIndex + x; i < lim; i++) {
+				dst.put(data[i]);
+			}
+			firstAvailIndex += x;
+			return x;
+		}
+	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/util/RegularFileStats.java	Wed Mar 21 20:51:12 2012 +0100
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2012 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.tmatesoft.hg.internal.Internals;
+import org.tmatesoft.hg.internal.ProcessExecHelper;
+
+/**
+ * Utility to collect executable files and symbolic links in a directory.
+ * 
+ * 
+ * Not public as present approach (expect file but collect once per directory) may need to be made explicit
+ * 
+ * TODO post-1.0 Add Linux-specific set of tests (similar to my test-flags repository, with symlink, executable and regular file,
+ * and few revisions where link and exec flags change. +testcase when link points to non-existing file (shall not report as missing, 
+ * iow either FileInfo.exist() shall respect symlinks or WCSC account for )
+ * 
+ * TODO post-1.0 Add extraction of link modification time, see RegularFileInfo#lastModified()
+ * 
+ * @author Artem Tikhomirov
+ * @author Tmate Software Ltd.
+ */
+/*package-local*/ class RegularFileStats {
+	private boolean isExec, isSymlink;
+	private String symlinkValue;
+	private final List<String> command;
+	private final ProcessExecHelper execHelper;
+	private final Matcher linkMatcher, execMatcher;
+	
+	
+	// directory name to (short link name -> link target)
+	private Map<String, Map<String, String>> dir2links = new TreeMap<String, Map<String, String>>();
+	// directory name to set of executable file short names
+	private Map<String, Set<String>> dir2execs = new TreeMap<String, Set<String>>();
+
+
+	RegularFileStats() {
+		if (Internals.runningOnWindows()) {
+			// XXX this implementation is not yet tested against any Windows repository, 
+			// only against sample dir listings. As long as Mercurial doesn't handle Windows
+			// links, we don't really need this
+			command = Arrays.asList("cmd", "/c", "dir");
+			// Windows patterns need to work against full directory listing (I didn't find a way 
+			// to list single file with its attributes like SYMLINK) 
+			Pattern pLink = Pattern.compile("^\\S+.*\\s+<SYMLINK>\\s+(\\S.*)\\s+\\[(.+)\\]$", Pattern.MULTILINE);
+			Pattern pExec = Pattern.compile("^\\S+.*\\s+\\d+\\s+(\\S.*\\.exe)$", Pattern.MULTILINE);
+			linkMatcher = pLink.matcher("");
+			execMatcher = pExec.matcher("");
+		} else {
+			command = Arrays.asList("/bin/ls", "-l", "-Q"); // -Q is essential to get quoted name - the only way to
+			// tell exact file name (which may start or end with spaces.
+			Pattern pLink = Pattern.compile("^lrwxrwxrwx\\s.*\\s\"(.*)\"\\s+->\\s+\"(.*)\"$", Pattern.MULTILINE);
+			// pLink: group(1) is full name if single file listing (ls -l /usr/bin/java) and short name if directory listing (ls -l /usr/bin)
+			//        group(2) is link target
+			Pattern pExec = Pattern.compile("^-..[sx]..[sx]..[sx]\\s.*\\s\"(.+)\"$", Pattern.MULTILINE);
+			// pExec: group(1) is name of executable file
+			linkMatcher = pLink.matcher("");
+			execMatcher = pExec.matcher("");
+		}
+		execHelper = new ProcessExecHelper();
+	}
+
+	public void init(File f) {
+		// can't check isFile because Java would say false for a symlink with non-existing target
+		if (f.isDirectory()) {
+			// perhaps, shall just collect stats for all files and set false to exec/link flags?
+			throw new IllegalArgumentException(); // FIXME EXCEPTIONS
+		}
+		final String dirName = f.getParentFile().getAbsolutePath();
+		final String fileName = f.getName();
+		Map<String, String> links = dir2links.get(dirName);
+		Set<String> execs = dir2execs.get(dirName);
+		if (links == null || execs == null) {
+			try {
+				ArrayList<String> cmd = new ArrayList<String>(command);
+				cmd.add(dirName);
+				CharSequence result = execHelper.exec(cmd);
+				
+				if (execMatcher.reset(result).find()) {
+					execs = new HashSet<String>();
+					do {
+						execs.add(execMatcher.group(1));
+					} while (execMatcher.find());
+				} else {
+					execs = Collections.emptySet(); // indicate we tried and found nothing
+				}
+				if (linkMatcher.reset(result).find()) {
+					links = new HashMap<String, String>();
+					do {
+						links.put(linkMatcher.group(1), linkMatcher.group(2));
+					} while (linkMatcher.find());
+				} else {
+					links = Collections.emptyMap();
+				}
+				dir2links.put(dirName, links);
+				dir2execs.put(dirName, execs);
+			} catch (InterruptedException ex) {
+				// try again? ensure not too long? stop right away?
+				// FIXME EXCEPTIONS
+				throw new RuntimeException();
+			} catch (IOException ex) {
+				// FIXME EXCEPTIONS perhaps, fail silently indicating false for both x and l?
+				throw new RuntimeException();
+			}
+		}
+		isExec = execs.contains(fileName);
+		isSymlink = links.containsKey(fileName);
+		if (isSymlink) {
+			symlinkValue = links.get(fileName);
+		} else {
+			symlinkValue = null;
+		}
+	}
+
+	public boolean isExecutable() {
+		return isExec;
+	}
+	
+	public boolean isSymlink() {
+		return isSymlink;
+	}
+
+	public String getSymlinkTarget() {
+		if (isSymlink) {
+			return symlinkValue;
+		}
+		throw new UnsupportedOperationException();
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/util/package.html	Wed Mar 21 20:51:12 2012 +0100
@@ -0,0 +1,6 @@
+<html>
+<body>
+<h2>Utility API</h2>
+<p>Miscellaneous utility classes not related directly to Mercurial repositories</p>
+</body>
+</html>
\ No newline at end of file
--- a/test/org/tmatesoft/hg/test/ExecHelper.java	Wed Mar 21 14:54:02 2012 +0100
+++ b/test/org/tmatesoft/hg/test/ExecHelper.java	Wed Mar 21 20:51:12 2012 +0100
@@ -18,84 +18,55 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.CharBuffer;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedList;
+import java.util.List;
 import java.util.StringTokenizer;
 
+import org.tmatesoft.hg.internal.ProcessExecHelper;
+
 /**
  *
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
-public class ExecHelper {
+public class ExecHelper extends ProcessExecHelper {
 
 	private final OutputParser parser;
-	private File dir;
-	private int exitValue;
 
 	public ExecHelper(OutputParser outParser, File workingDir) {
 		parser = outParser;
-		dir = workingDir;
+		super.cwd(workingDir);
 	}
-
-	public void run(String... cmd) throws IOException, InterruptedException {
-		ProcessBuilder pb = null;
+	
+	@Override
+	protected List<String> prepareCommand(List<String> cmd) {
+		String commandName = cmd.get(0);
 		if (System.getProperty("os.name").startsWith("Windows")) {
 			StringTokenizer st = new StringTokenizer(System.getenv("PATH"), ";");
 			while (st.hasMoreTokens()) {
 				File pe = new File(st.nextToken());
-				if (new File(pe, cmd[0] + ".exe").exists()) {
-					break;
+				if (new File(pe, commandName + ".exe").exists()) {
+					return cmd;
 				}
 				// PATHEXT controls precedence of .exe, .bat and .cmd files, usually .exe wins
-				if (new File(pe, cmd[0] + ".bat").exists() || new File(pe, cmd[0] + ".cmd").exists()) {
+				if (new File(pe, commandName + ".bat").exists() || new File(pe, commandName + ".cmd").exists()) {
 					ArrayList<String> command = new ArrayList<String>();
 					command.add("cmd.exe");
 					command.add("/C");
-					command.addAll(Arrays.asList(cmd));
-					pb = new ProcessBuilder(command);
-					break;
+					command.addAll(cmd);
+					return command;
 				}
 			}
 		}
-		if (pb == null) {
-			pb = new ProcessBuilder(cmd);
-		}
-		Process p = pb.directory(dir).redirectErrorStream(true).start();
-		InputStreamReader stdOut = new InputStreamReader(p.getInputStream());
-		LinkedList<CharBuffer> l = new LinkedList<CharBuffer>();
-		int r = -1;
-		CharBuffer b = null;
-		do {
-			if (b == null || b.remaining() < b.capacity() / 3) {
-				b = CharBuffer.allocate(512);
-				l.add(b);
-			}
-			r = stdOut.read(b);
-		} while (r != -1);
-		int total = 0;
-		for (CharBuffer cb : l) {
-			total += cb.position();
-			cb.flip();
-		}
-		CharBuffer res = CharBuffer.allocate(total);
-		for (CharBuffer cb : l) {
-			res.put(cb);
-		}
-		res.flip();
-		p.waitFor();
-		exitValue = p.exitValue();
+		return super.prepareCommand(cmd);
+	}
+	
+	public void run(String... cmd) throws IOException, InterruptedException {
+		CharSequence res = super.exec(cmd);
 		parser.parse(res);
 	}
-	
+
 	public int getExitValue() {
-		return exitValue;
-	}
-
-	public void cwd(File wd) {
-		dir = wd;
+		return super.exitValue();
 	}
 }