changeset 148:1a7a9a20e1f9

Exceptions, javadoc. Initial cancel and progress support
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 23 Feb 2011 22:36:28 +0100
parents a05145db4d0c
children 868a5a6e9f93
files src/org/tmatesoft/hg/core/HgBadStateException.java src/org/tmatesoft/hg/core/HgCatCommand.java src/org/tmatesoft/hg/core/HgChangeset.java src/org/tmatesoft/hg/core/HgDataStreamException.java src/org/tmatesoft/hg/core/HgException.java src/org/tmatesoft/hg/core/HgLogCommand.java src/org/tmatesoft/hg/core/HgManifestCommand.java src/org/tmatesoft/hg/core/HgRepoFacade.java src/org/tmatesoft/hg/core/HgStatusCommand.java src/org/tmatesoft/hg/core/Nodeid.java src/org/tmatesoft/hg/internal/ByteArrayChannel.java src/org/tmatesoft/hg/internal/FilterByteChannel.java src/org/tmatesoft/hg/repo/HgDataFile.java src/org/tmatesoft/hg/repo/HgInternals.java src/org/tmatesoft/hg/repo/HgLookup.java src/org/tmatesoft/hg/repo/HgRepository.java src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java src/org/tmatesoft/hg/util/Adaptable.java src/org/tmatesoft/hg/util/ByteChannel.java src/org/tmatesoft/hg/util/CancelSupport.java src/org/tmatesoft/hg/util/CancelledException.java src/org/tmatesoft/hg/util/Path.java src/org/tmatesoft/hg/util/ProgressSupport.java
diffstat 23 files changed, 481 insertions(+), 40 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/HgBadStateException.java	Wed Feb 23 22:36:28 2011 +0100
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2011 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.core;
+
+/**
+ * hg4j's own internal error or unexpected state.
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+@SuppressWarnings("serial")
+public class HgBadStateException extends RuntimeException {
+
+}
--- a/src/org/tmatesoft/hg/core/HgCatCommand.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/core/HgCatCommand.java	Wed Feb 23 22:36:28 2011 +0100
@@ -16,14 +16,17 @@
  */
 package org.tmatesoft.hg.core;
 
+import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision;
 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 org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.ByteChannel;
+import org.tmatesoft.hg.util.CancelledException;
 import org.tmatesoft.hg.util.Path;
 
 /**
@@ -43,25 +46,59 @@
 		repo = hgRepo;
 	}
 
+	/**
+	 * File to read, required parameter 
+	 * @param fname path to a repository file, can't be <code>null</code>
+	 * @return <code>this</code> for convenience
+	 * @throws IllegalArgumentException if supplied fname is null or points to directory
+	 */
 	public HgCatCommand file(Path fname) {
+		if (fname == null || fname.isDirectory()) {
+			throw new IllegalArgumentException(String.valueOf(fname));
+		}
 		file = fname;
 		return this;
 	}
 
-	// rev can't be WORKING_COPY (if allowed, need to implement in #execute())
+	/**
+	 * Invocation of this method clears revision set with {@link #revision(Nodeid)} or {@link #revision(int)} earlier.
+	 * XXX rev can't be WORKING_COPY (if allowed, need to implement in #execute())
+	 * @param rev local revision number, non-negative, or one of predefined constants. Note, use of {@link HgRepository#BAD_REVISION}, 
+	 * although possible, makes little sense (command would fail if executed).  
+ 	 * @return <code>this</code> for convenience
+	 */
 	public HgCatCommand revision(int rev) {
+		if (wrongLocalRevision(rev)) {
+			throw new IllegalArgumentException(String.valueOf(rev));
+		}
 		localRevision = rev;
 		revision = null;
 		return this;
 	}
 	
+	/**
+	 * Select revision to read. Invocation of this method clears revision set with {@link #revision(int)} or {@link #revision(Nodeid)} earlier.
+	 * 
+	 * @param nodeid - unique revision identifier, Note, use of <code>null</code> or {@link Nodeid#NULL} is senseless
+	 * @return <code>this</code> for convenience
+	 */
 	public HgCatCommand revision(Nodeid nodeid) {
+		if (nodeid != null && nodeid.isNull()) {
+			nodeid = null;
+		}
 		revision = nodeid;
 		localRevision = BAD_REVISION;
 		return this;
 	}
 
-	public void execute(ByteChannel sink) throws Exception /*TODO own exception type*/ {
+	/**
+	 * 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 IllegalArgumentException when command arguments are incomplete or wrong
+	 */
+	public void execute(ByteChannel sink) throws HgDataStreamException, IOException, CancelledException {
 		if (localRevision == BAD_REVISION && revision == null) {
 			throw new IllegalArgumentException("Either local file revision number or nodeid shall be specified");
 		}
@@ -69,11 +106,11 @@
 			throw new IllegalArgumentException("Name of the file is missing");
 		}
 		if (sink == null) {
-			throw new IllegalArgumentException();
+			throw new IllegalArgumentException("Need an output channel");
 		}
 		HgDataFile dataFile = repo.getFileNode(file);
 		if (!dataFile.exists()) {
-			throw new FileNotFoundException();
+			throw new HgDataStreamException(file.toString(), new FileNotFoundException(file.toString()));
 		}
 		int revToExtract;
 		if (revision != null) {
--- a/src/org/tmatesoft/hg/core/HgChangeset.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/core/HgChangeset.java	Wed Feb 23 22:36:28 2011 +0100
@@ -157,14 +157,14 @@
 		for (Path s : r.getModified()) {
 			Nodeid nid = r.nodeidAfterChange(s);
 			if (nid == null) {
-				throw new IllegalArgumentException();
+				throw new HgBadStateException();
 			}
 			modified.add(new FileRevision(repo, nid, s));
 		}
 		for (Path s : r.getAdded()) {
 			Nodeid nid = r.nodeidAfterChange(s);
 			if (nid == null) {
-				throw new IllegalArgumentException();
+				throw new HgBadStateException();
 			}
 			added.add(new FileRevision(repo, nid, s));
 		}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/HgDataStreamException.java	Wed Feb 23 22:36:28 2011 +0100
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2011 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.core;
+
+import org.tmatesoft.hg.repo.HgDataFile;
+
+/**
+ * Any erroneous state with @link {@link HgDataFile} input/output, read/write operations 
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+@SuppressWarnings("serial")
+public class HgDataStreamException extends HgException {
+
+	public HgDataStreamException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/HgException.java	Wed Feb 23 22:36:28 2011 +0100
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2011 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.core;
+
+/**
+ * Root class for all hg4j exceptions.
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+@SuppressWarnings("serial")
+public class HgException extends Exception {
+
+	public HgException(String reason) {
+		super(reason);
+	}
+
+	public HgException(String reason, Throwable cause) {
+		super(reason, cause);
+	}
+
+	public HgException(Throwable cause) {
+		super(cause);
+	}
+
+//	/* XXX CONSIDER capability to pass extra information about errors */
+//	public static class Status {
+//		public Status(String message, Throwable cause, int errorCode, Object extraData) {
+//		}
+//	}
+}
--- a/src/org/tmatesoft/hg/core/HgLogCommand.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/core/HgLogCommand.java	Wed Feb 23 22:36:28 2011 +0100
@@ -66,9 +66,11 @@
 	}
 
 	/**
-	 * Limit search to specified user. Multiple user names may be specified.
+	 * Limit search to specified user. Multiple user names may be specified. Once set, user names can't be 
+	 * cleared, use new command instance in such cases.
 	 * @param user - full or partial name of the user, case-insensitive, non-null.
 	 * @return <code>this</code> instance for convenience
+	 * @throws IllegalArgumentException when argument is null
 	 */
 	public HgLogCommand user(String user) {
 		if (user == null) {
@@ -83,9 +85,11 @@
 
 	/**
 	 * Limit search to specified branch. Multiple branch specification possible (changeset from any of these 
-	 * would be included in result). If unspecified, all branches are considered.
+	 * would be included in result). If unspecified, all branches are considered. There's no way to clean branch selection 
+	 * once set, create fresh new command instead.
 	 * @param branch - branch name, case-sensitive, non-null.
 	 * @return <code>this</code> instance for convenience
+	 * @throws IllegalArgumentException when branch argument is null
 	 */
 	public HgLogCommand branch(String branch) {
 		if (branch == null) {
@@ -120,8 +124,8 @@
 	/**
 	 * Limit to specified subset of Changelog, [min(rev1,rev2), max(rev1,rev2)], inclusive.
 	 * Revision may be specified with {@link HgRepository#TIP}  
-	 * @param rev1
-	 * @param rev2
+	 * @param rev1 - local revision number
+	 * @param rev2 - local revision number
 	 * @return <code>this</code> instance for convenience
 	 */
 	public HgLogCommand range(int rev1, int rev2) {
@@ -291,7 +295,8 @@
 		
 		/*package-local*/FileRevision(HgRepository hgRepo, Nodeid rev, Path p) {
 			if (hgRepo == null || rev == null || p == null) {
-				throw new IllegalArgumentException();
+				// since it's package local, it is our code to blame for non validated arguments
+				throw new HgBadStateException();
 			}
 			repo = hgRepo;
 			revision = rev;
--- a/src/org/tmatesoft/hg/core/HgManifestCommand.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/core/HgManifestCommand.java	Wed Feb 23 22:36:28 2011 +0100
@@ -53,6 +53,13 @@
 		repo = hgRepo;
 	}
 
+	/**
+	 * Parameterize command to visit revisions <code>[rev1..rev2]</code>.
+	 * @param rev1 - local revision number to start from. Non-negative. May be {@link HgRepository#TIP} (rev2 argument shall be {@link HgRepository#TIP} as well, then) 
+	 * @param rev2 - local revision number to end with, inclusive. Non-negative, greater or equal to rev1. May be {@link HgRepository#TIP}.
+	 * @return <code>this</code> for convenience.
+	 * @throws IllegalArgumentException if revision arguments are incorrect (see above).
+	 */
 	public HgManifestCommand range(int rev1, int rev2) {
 		// XXX if manifest range is different from that of changelog, need conversion utils (external?)
 		boolean badArgs = rev1 == BAD_REVISION || rev2 == BAD_REVISION || rev1 == WORKING_COPY || rev2 == WORKING_COPY;
@@ -88,6 +95,12 @@
 		return this;
 	}
 	
+	/**
+	 * Runs the command.
+	 * @param handler - callback to get the outcome
+	 * @throws IllegalArgumentException if handler is <code>null</code>
+	 * @throws ConcurrentModificationException if this command is already in use (running)
+	 */
 	public void execute(Handler handler) {
 		if (handler == null) {
 			throw new IllegalArgumentException();
--- a/src/org/tmatesoft/hg/core/HgRepoFacade.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/core/HgRepoFacade.java	Wed Feb 23 22:36:28 2011 +0100
@@ -23,6 +23,14 @@
 
 /**
  * Starting point for the library.
+ * <p>Sample use:
+ * <pre>
+ *  HgRepoFacade f = new HgRepoFacade();
+ *  f.initFrom(System.getenv("whatever.repo.location"));
+ *  HgStatusCommand statusCmd = f.createStatusCommand();
+ *  HgStatusCommand.Handler handler = ...;
+ *  statusCmd.execute(handler);
+ * </pre>
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
@@ -33,6 +41,11 @@
 	public HgRepoFacade() {
 	}
 	
+	/**
+	 * @param hgRepo
+	 * @return true on successful initialization
+	 * @throws IllegalArgumentException when argument is null 
+	 */
 	public boolean init(HgRepository hgRepo) {
 		if (hgRepo == null) {
 			throw new IllegalArgumentException();
@@ -41,13 +54,29 @@
 		return !repo.isInvalid();
 	}
 
-	public boolean init() throws Exception /*FIXME RepoInitException*/ {
+	/**
+	 * Tries to find repository starting from the current working directory.
+	 * @return <code>true</code> if found valid repository
+	 * @throws HgException in case of errors during repository initialization
+	 */
+	public boolean init() throws HgException {
 		repo = new HgLookup().detectFromWorkingDir();
 		return repo != null && !repo.isInvalid();
 	}
 	
-	public boolean initFrom(File repoLocation) throws Exception {
-		repo = new HgLookup().detect(repoLocation.getCanonicalPath());
+	/**
+	 * Looks up Mercurial repository starting from specified location and up to filesystem root.
+	 * 
+	 * @param repoLocation path to any folder within structure of a Mercurial repository.
+	 * @return <code>true</code> if found valid repository 
+	 * @throws HgException if there are errors accessing specified location
+	 * @throws IllegalArgumentException if argument is <code>null</code>
+	 */
+	public boolean initFrom(File repoLocation) throws HgException {
+		if (repoLocation == null) {
+			throw new IllegalArgumentException();
+		}
+		repo = new HgLookup().detect(repoLocation);
 		return repo != null && !repo.isInvalid();
 	}
 	
--- a/src/org/tmatesoft/hg/core/HgStatusCommand.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/core/HgStatusCommand.java	Wed Feb 23 22:36:28 2011 +0100
@@ -17,6 +17,7 @@
 package org.tmatesoft.hg.core;
 
 import static org.tmatesoft.hg.core.HgStatus.Kind.*;
+import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision;
 import static org.tmatesoft.hg.repo.HgRepository.*;
 
 import java.util.ConcurrentModificationException;
@@ -93,15 +94,15 @@
 	}
 	
 	/**
-	 * if set, either base:revision or base:workingdir
+	 * If set, either base:revision or base:workingdir
 	 * to unset, pass {@link HgRepository#TIP} or {@link HgRepository#BAD_REVISION}
-	 * @param revision
-	 * @return
+	 * @param revision - local revision number to base status from
+	 * @return <code>this</code> for convenience
+	 * @throws IllegalArgumentException when revision is negative or {@link HgRepository#WORKING_COPY} 
 	 */
-	
 	public HgStatusCommand base(int revision) {
-		if (revision == WORKING_COPY) {
-			throw new IllegalArgumentException();
+		if (revision == WORKING_COPY || wrongLocalRevision(revision)) {
+			throw new IllegalArgumentException(String.valueOf(revision));
 		}
 		if (revision == BAD_REVISION) {
 			revision = TIP;
@@ -113,14 +114,15 @@
 	/**
 	 * Revision without base == --change
 	 * Pass {@link HgRepository#WORKING_COPY} or {@link HgRepository#BAD_REVISION} to reset
-	 * @param revision
-	 * @return
+	 * @param revision - non-negative local revision number, or any of {@link HgRepository#BAD_REVISION}, {@link HgRepository#WORKING_COPY} or {@link HgRepository#TIP}  
+	 * @return <code>this</code> for convenience
+	 * @throws IllegalArgumentException if local revision number doesn't specify legitimate revision. 
 	 */
 	public HgStatusCommand revision(int revision) {
 		if (revision == BAD_REVISION) {
 			revision = WORKING_COPY;
 		}
-		if (revision != TIP && revision != WORKING_COPY && revision < 0) {
+		if (wrongLocalRevision(revision)) {
 			throw new IllegalArgumentException(String.valueOf(revision));
 		}
 		endRevision = revision;
@@ -131,14 +133,19 @@
 	 * Shorthand for {@link #base(int) cmd.base(BAD_REVISION)}{@link #change(int) .revision(revision)}
 	 *  
 	 * @param revision compare given revision against its parent
-	 * @return
+	 * @return <code>this</code> for convenience
 	 */
 	public HgStatusCommand change(int revision) {
 		base(BAD_REVISION);
 		return revision(revision);
 	}
 	
-	// pass null to reset
+	/**
+	 * Limit status operation to certain sub-tree.
+	 * 
+	 * @param pathMatcher - matcher to use,  pass <code>null/<code> to reset
+	 * @return <code>this</code> for convenience
+	 */
 	public HgStatusCommand match(Path.Matcher pathMatcher) {
 		mediator.matcher = pathMatcher;
 		return this;
--- a/src/org/tmatesoft/hg/core/Nodeid.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/core/Nodeid.java	Wed Feb 23 22:36:28 2011 +0100
@@ -41,7 +41,8 @@
 
 	/**
 	 * @param binaryRepresentation - array of exactly 20 bytes
-	 * @param shallClone - true if array is subject to future modification and shall be copied, not referenced 
+	 * @param shallClone - true if array is subject to future modification and shall be copied, not referenced
+	 * @throws IllegalArgumentException if supplied binary representation doesn't correspond to 20 bytes of sha1 digest 
 	 */
 	public Nodeid(byte[] binaryRepresentation, boolean shallClone) {
 		// 5 int fields => 32 bytes
@@ -98,8 +99,14 @@
 		return binaryData.clone();
 	}
 
-	// primary difference with cons is handling of NULL id (this method returns constant)
-	// always makes a copy of an array passed
+	/**
+	 * Factory for {@link Nodeid Nodeids}.
+	 * Primary difference with cons is handling of NULL id (this method returns constant) and control over array 
+	 * duplication - this method always makes a copy of an array passed
+	 * @param binaryRepresentation - byte array of a length at least offset + 20
+	 * @param offset - index in the array to start from
+	 * @throws IllegalArgumentException when arguments don't select 20 bytes
+	 */
 	public static Nodeid fromBinary(byte[] binaryRepresentation, int offset) {
 		if (binaryRepresentation == null || binaryRepresentation.length - offset < 20) {
 			throw new IllegalArgumentException();
@@ -117,6 +124,13 @@
 		return new Nodeid(b, false);
 	}
 
+	/**
+	 * Parse encoded representation.
+	 * 
+	 * @param asciiRepresentation - encoded form of the Nodeid.
+	 * @return object representation
+	 * @throws IllegalArgumentException when argument doesn't match encoded form of 20-bytes sha1 digest. 
+	 */
 	public static Nodeid fromAscii(String asciiRepresentation) {
 		if (asciiRepresentation.length() != 40) {
 			throw new IllegalArgumentException();
@@ -124,6 +138,10 @@
 		// XXX is better impl for String possible?
 		return fromAscii(asciiRepresentation.getBytes(), 0, 40);
 	}
+
+	/**
+	 * Parse encoded representation. Similar to {@link #fromAscii(String)}.
+	 */
 	public static Nodeid fromAscii(byte[] asciiRepresentation, int offset, int length) {
 		if (length != 40) {
 			throw new IllegalArgumentException();
--- a/src/org/tmatesoft/hg/internal/ByteArrayChannel.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/ByteArrayChannel.java	Wed Feb 23 22:36:28 2011 +0100
@@ -49,7 +49,7 @@
 	}
 
 	// TODO document what happens on write after toArray() in each case
-	public int write(ByteBuffer buffer) throws Exception {
+	public int write(ByteBuffer buffer) {
 		int rv = buffer.remaining();
 		if (buffers == null) {
 			target.put(buffer);
--- a/src/org/tmatesoft/hg/internal/FilterByteChannel.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/FilterByteChannel.java	Wed Feb 23 22:36:28 2011 +0100
@@ -16,10 +16,12 @@
  */
 package org.tmatesoft.hg.internal;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.Collection;
 
 import org.tmatesoft.hg.util.ByteChannel;
+import org.tmatesoft.hg.util.CancelledException;
 
 /**
  *
@@ -38,7 +40,7 @@
 		filters = filtersToApply.toArray(new Filter[filtersToApply.size()]);
 	}
 
-	public int write(ByteBuffer buffer) throws Exception {
+	public int write(ByteBuffer buffer) throws IOException, CancelledException {
 		final int srcPos = buffer.position();
 		ByteBuffer processed = buffer;
 		for (Filter f : filters) {
--- a/src/org/tmatesoft/hg/repo/HgDataFile.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgDataFile.java	Wed Feb 23 22:36:28 2011 +0100
@@ -16,18 +16,26 @@
  */
 package org.tmatesoft.hg.repo;
 
+import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision;
+import static org.tmatesoft.hg.repo.HgRepository.*;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
+import static org.tmatesoft.hg.repo.HgRepository.WORKING_COPY;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.TreeMap;
 
+import org.tmatesoft.hg.core.HgDataStreamException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.internal.FilterByteChannel;
 import org.tmatesoft.hg.internal.RevlogStream;
 import org.tmatesoft.hg.util.ByteChannel;
+import org.tmatesoft.hg.util.CancelSupport;
+import org.tmatesoft.hg.util.CancelledException;
 import org.tmatesoft.hg.util.Path;
+import org.tmatesoft.hg.util.ProgressSupport;
 
 
 
@@ -75,21 +83,28 @@
 	}
 	
 	/*XXX not sure applyFilters is the best way to do, perhaps, callers shall add filters themselves?*/
-	public void content(int revision, ByteChannel sink, boolean applyFilters) throws /*TODO typed*/Exception {
+	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.
 	}
 
 	// for data files need to check heading of the file content for possible metadata
@@ -99,6 +114,9 @@
 		if (revision == TIP) {
 			revision = getLastRevision();
 		}
+		if (wrongLocalRevision(revision) || revision == BAD_REVISION || revision == WORKING_COPY) {
+			throw new IllegalArgumentException(String.valueOf(revision));
+		}
 		byte[] data = super.content(revision);
 		if (metadata == null) {
 			metadata = new Metadata();
--- a/src/org/tmatesoft/hg/repo/HgInternals.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgInternals.java	Wed Feb 23 22:36:28 2011 +0100
@@ -16,6 +16,8 @@
  */
 package org.tmatesoft.hg.repo;
 
+import static org.tmatesoft.hg.repo.HgRepository.*;
+
 import java.io.File;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -84,4 +86,9 @@
 			return username;
 		}
 	}
+
+	// Convenient check of local revision number for validity (not all negative values are wrong as long as we use negative constants)
+	public static boolean wrongLocalRevision(int rev) {
+		return rev < 0 && rev != TIP && rev != WORKING_COPY && rev != BAD_REVISION; 
+	}
 }
--- a/src/org/tmatesoft/hg/repo/HgLookup.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgLookup.java	Wed Feb 23 22:36:28 2011 +0100
@@ -17,24 +17,28 @@
 package org.tmatesoft.hg.repo;
 
 import java.io.File;
+import java.io.IOException;
+
+import org.tmatesoft.hg.core.HgException;
 
 /**
+ * Utility methods to find Mercurial repository at a given location
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
 public class HgLookup {
 
-	public HgRepository detectFromWorkingDir() throws Exception {
+	public HgRepository detectFromWorkingDir() throws HgException {
 		return detect(System.getProperty("user.dir"));
 	}
 
-	public HgRepository detect(String location) throws Exception /*FIXME Exception type, RepoInitException? */ {
+	public HgRepository detect(String location) throws HgException {
 		return detect(new File(location));
 	}
 
 	// look up in specified location and above
-	public HgRepository detect(File location) throws Exception {
+	public HgRepository detect(File location) throws HgException {
 		File dir = location;
 		File repository;
 		do {
@@ -50,6 +54,11 @@
 			// return invalid repository
 			return new HgRepository(location.getPath());
 		}
-		return new HgRepository(repository);
+		try {
+			String repoPath = repository.getParentFile().getCanonicalPath();
+			return new HgRepository(repoPath, repository);
+		} catch (IOException ex) {
+			throw new HgException(location.toString(), ex);
+		}
 	}
 }
--- a/src/org/tmatesoft/hg/repo/HgRepository.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgRepository.java	Wed Feb 23 22:36:28 2011 +0100
@@ -45,6 +45,7 @@
  */
 public final class HgRepository {
 
+	// if new constants added, consider fixing HgInternals#badLocalRevision
 	public static final int TIP = -1;
 	public static final int BAD_REVISION = Integer.MIN_VALUE;
 	public static final int WORKING_COPY = -2;
@@ -53,7 +54,7 @@
 	public static IllegalStateException notImplemented() {
 		return new IllegalStateException("Not implemented");
 	}
-
+	
 	private final File repoDir; // .hg folder
 	private final String repoLocation;
 	private final DataAccessProvider dataAccess;
@@ -79,10 +80,12 @@
 		normalizePath = null;
 	}
 	
-	HgRepository(File repositoryRoot) throws IOException {
+	HgRepository(String repositoryPath, File repositoryRoot) {
 		assert ".hg".equals(repositoryRoot.getName()) && repositoryRoot.isDirectory();
+		assert repositoryPath != null; 
+		assert repositoryRoot != null;
 		repoDir = repositoryRoot;
-		repoLocation = repositoryRoot.getParentFile().getCanonicalPath();
+		repoLocation = repositoryPath;
 		dataAccess = new DataAccessProvider();
 		final boolean runningOnWindows = System.getProperty("os.name").indexOf("Windows") != -1;
 		if (runningOnWindows) {
--- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Wed Feb 23 22:36:28 2011 +0100
@@ -262,7 +262,7 @@
 				ByteChannel check = new ByteChannel() {
 					int x = 0;
 					final boolean debug = false; // XXX may want to add global variable to allow clients to turn 
-					public int write(ByteBuffer buffer) throws Exception {
+					public int write(ByteBuffer buffer) {
 						for (int i = buffer.remaining(); i > 0; i--, x++) {
 							if (data[x] != buffer.get()) {
 								if (debug) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/util/Adaptable.java	Wed Feb 23 22:36:28 2011 +0100
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2011 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.util;
+
+/**
+ * Extension mechanism.
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public interface Adaptable {
+
+	<T> T getAdapter(Class<T> adapterClass);
+}
--- a/src/org/tmatesoft/hg/util/ByteChannel.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/util/ByteChannel.java	Wed Feb 23 22:36:28 2011 +0100
@@ -16,6 +16,7 @@
  */
 package org.tmatesoft.hg.util;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 
 /**
@@ -30,5 +31,5 @@
 	// XXX does int return value makes any sense given buffer keeps its read state
 	// not clear what retvalue should be in case some filtering happened inside write - i.e. return
 	// number of bytes consumed in 
-	int write(ByteBuffer buffer) throws Exception /*FIXME Exception type*/;
+	int write(ByteBuffer buffer) throws IOException, CancelledException;
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/util/CancelSupport.java	Wed Feb 23 22:36:28 2011 +0100
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2011 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.util;
+
+/**
+ * Mix-in for objects that support cancellation. 
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public interface CancelSupport {
+
+	/**
+	 * This method is invoked to check if target had been brought to canceled state. Shall silently return if target is
+	 * in regular state.
+	 * @throws CancelledException when target internal state has been changed to canceled.
+	 */
+	void checkCancelled() throws CancelledException;
+
+
+	// Yeah, this factory class looks silly now, but perhaps in the future I'll need wrappers for other cancellation sources?
+	// just don't want to have general Utils class with methods like get() below
+	static class Factory {
+
+		/**
+		 * Obtain non-null cancel support object.
+		 * 
+		 * @param target any object (or <code>null</code>) that might have cancel support
+		 * @return target if it's capable checking cancellation status or no-op implementation that never cancels.
+				 */
+		public static CancelSupport get(Object target) {
+			if (target instanceof  CancelSupport) {
+				return (CancelSupport) target;
+			}
+			if (target instanceof Adaptable) {
+				CancelSupport cs = ((Adaptable) target).getAdapter(CancelSupport.class);
+				if (cs != null) {
+					return cs;
+				}
+			}
+			return new CancelSupport() {
+				public void checkCancelled() {
+				}
+			};
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/util/CancelledException.java	Wed Feb 23 22:36:28 2011 +0100
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2011 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.util;
+
+/**
+ *
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+@SuppressWarnings("serial")
+public class CancelledException extends Exception {
+
+	public CancelledException() {
+	}
+}
--- a/src/org/tmatesoft/hg/util/Path.java	Tue Feb 22 15:49:26 2011 +0100
+++ b/src/org/tmatesoft/hg/util/Path.java	Wed Feb 23 22:36:28 2011 +0100
@@ -31,6 +31,17 @@
 		path = p;
 	}
 
+	/**
+	 * Check if this is directory's path. 
+	 * Note, this method doesn't perform any file system operation.
+	 * 
+	 * @return true when this path points to a directory 
+	 */
+	public boolean isDirectory() {
+		// XXX simple logic for now. Later we may decide to have an explicit factory method to create directory paths
+		return path.charAt(path.length() - 1) == '/';
+	}
+
 	public int length() {
 		return path.length();
 	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/util/ProgressSupport.java	Wed Feb 23 22:36:28 2011 +0100
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2011 TMate Software Ltd
+ *  
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * For information on how to redistribute this software under
+ * the terms of a license other than GNU General Public License
+ * contact TMate Software at support@hg4j.com
+ */
+package org.tmatesoft.hg.util;
+
+/**
+ * Mix-in to report progress of a long-running operation
+ *  
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public interface ProgressSupport {
+
+	public void start(int totalUnits);
+	public void worked(int units);
+	public void done();
+
+	static class Factory {
+
+		/**
+		 * @param target object that might be capable to report progress. Can be <code>null</code>
+		 * @return support object extracted from target or an empty, no-op implementation
+		 */
+		public static ProgressSupport get(Object target) {
+			if (target instanceof ProgressSupport) {
+				return (ProgressSupport) target;
+			}
+			if (target instanceof Adaptable) {
+				ProgressSupport ps = ((Adaptable) target).getAdapter(ProgressSupport.class);
+				if (ps != null) {
+					return ps;
+				}
+			}
+			return new ProgressSupport() {
+				public void start(int totalUnits) {
+				}
+				public void worked(int units) {
+				}
+				public void done() {
+				}
+			};
+		}
+	}
+}