changeset 520:1ee452f31187

Experimental support for inverse direction history walking. Refactored/streamlined cancellation in HgLogCommand and down the stack
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 21 Dec 2012 21:20:26 +0100
parents 934037edbea0
children 59e555c85da0
files src/org/tmatesoft/hg/core/ChangesetTransformer.java src/org/tmatesoft/hg/core/HgLogCommand.java src/org/tmatesoft/hg/internal/AdapterPlug.java src/org/tmatesoft/hg/internal/BatchRangeHelper.java src/org/tmatesoft/hg/internal/IterateControlMediator.java src/org/tmatesoft/hg/internal/LifecycleBridge.java src/org/tmatesoft/hg/internal/LifecycleProxy.java src/org/tmatesoft/hg/internal/RevlogStream.java src/org/tmatesoft/hg/repo/HgChangelog.java test/org/tmatesoft/hg/test/ErrorCollectorExt.java test/org/tmatesoft/hg/test/TestAuxUtilities.java test/org/tmatesoft/hg/test/TestHistory.java
diffstat 12 files changed, 698 insertions(+), 143 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/ChangesetTransformer.java	Thu Dec 20 20:21:59 2012 +0100
+++ b/src/org/tmatesoft/hg/core/ChangesetTransformer.java	Fri Dec 21 21:20:26 2012 +0100
@@ -18,6 +18,8 @@
 
 import java.util.Set;
 
+import org.tmatesoft.hg.internal.Lifecycle;
+import org.tmatesoft.hg.internal.LifecycleBridge;
 import org.tmatesoft.hg.internal.PathPool;
 import org.tmatesoft.hg.repo.HgChangelog;
 import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
@@ -37,14 +39,12 @@
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
-/*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector, Adaptable, CancelSupport {
+/*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector, Adaptable {
 	private final HgChangesetHandler handler;
-	private final ProgressSupport progressHelper;
-	private final CancelSupport cancelHelper;
+	private final LifecycleBridge lifecycleBridge;
 	private final Transformation t;
 	private Set<String> branches;
 	private HgCallbackTargetException failure;
-	private CancelledException cancellation;
 
 	// repo and delegate can't be null, parent walker can
 	// ps and cs can't be null
@@ -58,11 +58,9 @@
 		HgStatusCollector statusCollector = new HgStatusCollector(hgRepo);
 		t = new Transformation(statusCollector, pw);
 		handler = delegate;
-		// we let HgChangelog#range deal with progress (pipe through getAdapter)
-		// but use own cancellation (which involves CallbackTargetException as well, and preserves original cancellation 
-		// exception in case clients care)
-		cancelHelper = cs;
-		progressHelper = ps;
+		// lifecycleBridge takes care of progress and cancellation, plus
+		// gives us explicit way to stop iteration (once HgCallbackTargetException) comes.
+		lifecycleBridge = new LifecycleBridge(ps, cs);
 	}
 	
 	public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
@@ -73,23 +71,21 @@
 		HgChangeset changeset = t.handle(revisionNumber, nodeid, cset);
 		try {
 			handler.cset(changeset);
-			cancelHelper.checkCancelled();
+			lifecycleBridge.nextStep();
 		} catch (HgCallbackTargetException ex) {
 			failure = ex.setRevision(nodeid).setRevisionIndex(revisionNumber);
-		} catch (CancelledException ex) {
-			cancellation = ex;
+			lifecycleBridge.stop();
 		}
 	}
 	
 	public void checkFailure() throws HgCallbackTargetException, CancelledException {
 		if (failure != null) {
 			HgCallbackTargetException toThrow = failure;
-			failure = null; // just in (otherwise unexpected) case this instance would get reused
 			throw toThrow;
 		}
-		if (cancellation != null) {
-			CancelledException toThrow = cancellation;
-			cancellation = null;
+		if (lifecycleBridge.isCancelled()) {
+			CancelledException toThrow = lifecycleBridge.getCancelOrigin();
+			assert toThrow != null;
 			throw toThrow;
 		}
 	}
@@ -121,18 +117,11 @@
 		}
 	}
 
-	public void checkCancelled() throws CancelledException {
-		if (failure != null || cancellation != null) {
-			// stop HgChangelog.Iterator. Our exception is for the purposes of cancellation only,
-			// the one we have stored (this.cancellation) is for user
-			throw new CancelledException(); 
+	public <T> T getAdapter(Class<T> adapterClass) {
+		if (adapterClass == Lifecycle.class) {
+			return adapterClass.cast(lifecycleBridge);
 		}
-	}
-
-	public <T> T getAdapter(Class<T> adapterClass) {
-		if (adapterClass == ProgressSupport.class) {
-			return adapterClass.cast(progressHelper);
-		}
-		return null;
+		// just in case there are more adapters in future
+		return Adaptable.Factory.getAdapter(handler, adapterClass, null);
 	}
 }
\ No newline at end of file
--- a/src/org/tmatesoft/hg/core/HgLogCommand.java	Thu Dec 20 20:21:59 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgLogCommand.java	Fri Dec 21 21:20:26 2012 +0100
@@ -34,9 +34,13 @@
 import java.util.Set;
 import java.util.TreeSet;
 
+import org.tmatesoft.hg.internal.AdapterPlug;
+import org.tmatesoft.hg.internal.BatchRangeHelper;
+import org.tmatesoft.hg.internal.Experimental;
 import org.tmatesoft.hg.internal.IntMap;
 import org.tmatesoft.hg.internal.IntVector;
 import org.tmatesoft.hg.internal.Lifecycle;
+import org.tmatesoft.hg.internal.LifecycleProxy;
 import org.tmatesoft.hg.repo.HgChangelog;
 import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
 import org.tmatesoft.hg.repo.HgDataFile;
@@ -287,19 +291,41 @@
 			throw new HgBadArgumentException(String.format("Bad value %d for start revision for range [%1$d..%d]", startRev, lastCset), null);
 		}
 		final ProgressSupport progressHelper = getProgressSupport(handler);
+		final int BATCH_SIZE = 100;
 		try {
 			count = 0;
 			HgParentChildMap<HgChangelog> pw = getParentHelper(file == null); // leave it uninitialized unless we iterate whole repo
 			// ChangesetTransfrom creates a blank PathPool, and #file(String, boolean) above 
 			// may utilize it as well. CommandContext? How about StatusCollector there as well?
 			csetTransform = new ChangesetTransformer(repo, handler, pw, progressHelper, getCancelSupport(handler, true));
+			// FilteringInspector is responsible to check command arguments: users, branches, limit, etc.
+			// prior to passing cset to next Inspector, which is either (a) collector to reverse cset order, then invokes 
+			// transformer from (b), below, with alternative cset order or (b) transformer to hi-level csets. 
 			FilteringInspector filterInsp = new FilteringInspector();
 			filterInsp.changesets(startRev, lastCset);
 			if (file == null) {
-				progressHelper.start(endRev - startRev + 1);
-				repo.getChangelog().range(startRev, endRev, filterInsp);
-				csetTransform.checkFailure();
+				progressHelper.start(lastCset - startRev + 1);
+				if (iterateDirection == IterateDirection.FromOldToNew) {
+					filterInsp.delegateTo(csetTransform);
+					repo.getChangelog().range(startRev, lastCset, filterInsp);
+					csetTransform.checkFailure();
+				} else {
+					assert iterateDirection == IterateDirection.FromNewToOld;
+					BatchRangeHelper brh = new BatchRangeHelper(startRev, lastCset, BATCH_SIZE, true);
+					BatchChangesetInspector batchInspector = new BatchChangesetInspector(Math.min(lastCset-startRev+1, BATCH_SIZE));
+					filterInsp.delegateTo(batchInspector);
+					while (brh.hasNext()) {
+						brh.next();
+						repo.getChangelog().range(brh.start(), brh.end(), filterInsp);
+						for (BatchChangesetInspector.BatchRecord br : batchInspector.iterate(true)) {
+							csetTransform.next(br.csetIndex, br.csetRevision, br.cset);
+							csetTransform.checkFailure();
+						}
+						batchInspector.reset();
+					}
+				}
 			} else {
+				filterInsp.delegateTo(csetTransform);
 				final HgFileRenameHandlerMixin withCopyHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
 				List<Pair<HgDataFile, Nodeid>> fileRenames = buildFileRenamesQueue();
 				progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/);
@@ -309,10 +335,12 @@
 					HgDataFile fileNode = curRename.first();
 					if (followAncestry) {
 						TreeBuildInspector treeBuilder = new TreeBuildInspector(followAncestry);
+						@SuppressWarnings("unused")
 						List<HistoryNode> fileAncestry = treeBuilder.go(fileNode, curRename.second());
 						int[] commitRevisions = narrowChangesetRange(treeBuilder.getCommitRevisions(), startRev, lastCset);
 						if (iterateDirection == IterateDirection.FromOldToNew) {
 							repo.getChangelog().range(filterInsp, commitRevisions);
+							csetTransform.checkFailure();
 						} else {
 							assert iterateDirection == IterateDirection.FromNewToOld;
 							// visit one by one in the opposite direction
@@ -325,8 +353,24 @@
 						// report complete file history (XXX may narrow range with [startRev, endRev], but need to go from file rev to link rev)
 						int fileStartRev = 0; //fileNode.getChangesetRevisionIndex(0) >= startRev
 						int fileEndRev = fileNode.getLastRevision();
-						fileNode.history(fileStartRev, fileEndRev, filterInsp);
-						csetTransform.checkFailure();
+						if (iterateDirection == IterateDirection.FromOldToNew) {
+							fileNode.history(fileStartRev, fileEndRev, filterInsp);
+							csetTransform.checkFailure();
+						} else {
+							assert iterateDirection == IterateDirection.FromNewToOld;
+							BatchRangeHelper brh = new BatchRangeHelper(fileStartRev, fileEndRev, BATCH_SIZE, true);
+							BatchChangesetInspector batchInspector = new BatchChangesetInspector(Math.min(fileEndRev-fileStartRev+1, BATCH_SIZE));
+							filterInsp.delegateTo(batchInspector);
+							while (brh.hasNext()) {
+								brh.next();
+								fileNode.history(brh.start(), brh.end(), filterInsp);
+								for (BatchChangesetInspector.BatchRecord br : batchInspector.iterate(true /*iterateDirection == IterateDirection.FromNewToOld*/)) {
+									csetTransform.next(br.csetIndex, br.csetRevision, br.cset);
+									csetTransform.checkFailure();
+								}
+								batchInspector.reset();
+							}
+						}
 					}
 					if (followRenames && withCopyHandler != null && nameIndex + 1 < fileRenamesSize) {
 						Pair<HgDataFile, Nodeid> nextRename = fileRenames.get(nameIndex+1);
@@ -354,6 +398,46 @@
 		}
 	}
 	
+	private static class BatchChangesetInspector extends AdapterPlug implements HgChangelog.Inspector {
+		private static class BatchRecord {
+			public final int csetIndex;
+			public final Nodeid csetRevision;
+			public final RawChangeset cset;
+			
+			public BatchRecord(int index, Nodeid nodeid, RawChangeset changeset) {
+				csetIndex = index;
+				csetRevision = nodeid;
+				cset = changeset;
+			}
+		}
+		private final ArrayList<BatchRecord> batch;
+
+		public BatchChangesetInspector(int batchSizeHint) {
+			batch = new ArrayList<BatchRecord>(batchSizeHint);
+		}
+
+		public BatchChangesetInspector reset() {
+			batch.clear();
+			return this;
+		}
+		
+		public void next(int revisionIndex, Nodeid nodeid, RawChangeset cset) {
+			batch.add(new BatchRecord(revisionIndex, nodeid, cset.clone()));
+		}
+		
+		public Iterable<BatchRecord> iterate(final boolean reverse) {
+			return new Iterable<BatchRecord>() {
+				
+				public Iterator<BatchRecord> iterator() {
+					return reverse ? new ReverseIterator<BatchRecord>(batch) : batch.iterator();
+				}
+			};
+		}
+		
+		// alternative would be dispatch(HgChangelog.Inspector) and dispatchReverse()
+		// methods, but progress and cancellation might get messy then
+	}
+	
 //	public static void main(String[] args) {
 //		int[] r = new int[] {17, 19, 21, 23, 25, 29};
 //		System.out.println(Arrays.toString(narrowChangesetRange(r, 0, 45)));
@@ -454,6 +538,21 @@
 		progressHelper.done();
 	}
 	
+	/**
+	 * DO NOT USE THIS METHOD, DEBUG PURPOSES ONLY!!!
+	 */
+	@Experimental(reason="Work in progress")
+	public HgLogCommand debugSwitch1() {
+		// FIXME can't expose iteration direction unless general iteration (changelog, not a file) supports it, too.
+		// however, need to test the code already there, hence this debug switch
+		if (iterateDirection == IterateDirection.FromOldToNew) {
+			iterateDirection = IterateDirection.FromNewToOld;
+		} else {
+			iterateDirection = IterateDirection.FromOldToNew;
+		}
+		return this;
+	}
+	
 	private IterateDirection iterateDirection = IterateDirection.FromOldToNew;
 
 	private static class ReverseIterator<E> implements Iterator<E> {
@@ -820,16 +919,29 @@
 
 	//
 	
-	private class FilteringInspector implements HgChangelog.Inspector, Lifecycle {
+	private class FilteringInspector extends AdapterPlug implements HgChangelog.Inspector, Adaptable {
 	
-		private Callback lifecycle;
 		private int firstCset = BAD_REVISION, lastCset = BAD_REVISION;
+		private HgChangelog.Inspector delegate;
+		// we use lifecycle to stop when limit is reached.
+		// delegate, however, may use lifecycle, too, so give it a chance
+		private LifecycleProxy lifecycleProxy;
 		
 		// limit to changesets in this range only
 		public void changesets(int start, int end) {
 			firstCset = start;
 			lastCset = end;
 		}
+		
+		public void delegateTo(HgChangelog.Inspector inspector) {
+			delegate = inspector;
+			// let delegate control life cycle, too
+			if (lifecycleProxy == null) {
+				super.attachAdapter(Lifecycle.class, lifecycleProxy = new LifecycleProxy(inspector));
+			} else {
+				lifecycleProxy.init(inspector);
+			}
+		}
 
 		public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
 			if (limit > 0 && count >= limit) {
@@ -862,22 +974,14 @@
 			if (date != null) {
 				// TODO post-1.0 implement date support for log
 			}
-			csetTransform.next(revisionNumber, nodeid, cset);
-			if (++count >= limit) {
-				if (lifecycle != null) { // FIXME support Lifecycle delegation
-				lifecycle.stop();
-				}
+			delegate.next(revisionNumber, nodeid, cset);
+			count++;
+			if (limit > 0 && count >= limit) {
+				lifecycleProxy.stop();
 			}
 		}
+	}
 
-		public void start(int count, Callback callback, Object token) {
-			lifecycle = callback;
-		}
-
-		public void finish(Object token) {
-		}
-	}
-	
 	private HgParentChildMap<HgChangelog> getParentHelper(boolean create) throws HgInvalidControlFileException {
 		if (parentHelper == null && create) {
 			parentHelper = new HgParentChildMap<HgChangelog>(repo.getChangelog());
@@ -885,8 +989,7 @@
 		}
 		return parentHelper;
 	}
-
-
+	
 	public static class CollectHandler implements HgChangesetHandler {
 		private final List<HgChangeset> result = new LinkedList<HgChangeset>();
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/AdapterPlug.java	Fri Dec 21 21:20:26 2012 +0100
@@ -0,0 +1,59 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.tmatesoft.hg.util.Adaptable;
+
+/**
+ * Implementation of {@link Adaptable} that allows to add ("plug")
+ * adapters as needed
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class AdapterPlug implements Adaptable {
+	private final Map<Class<?>, Object> adapters = new HashMap<Class<?>, Object>();
+	private final List<Class<?>> adapterUses = new ArrayList<Class<?>>();
+	
+	public <T> void attachAdapter(Class<T> adapterClass, T instance) {
+		adapters.put(adapterClass, instance);
+	}
+
+	public <T> T getAdapter(Class<T> adapterClass) {
+		Object instance = adapters.get(adapterClass);
+		if (instance != null) {
+			adapterUses.add(adapterClass);
+			return adapterClass.cast(instance);
+		}
+		return null;
+	}
+	
+	public int getAdapterUse(Class<?> adapterClass) {
+		int uses = 0;
+		for (Class<?> c : adapterUses) {
+			if (c == adapterClass) {
+				uses++;
+			}
+		}
+		return uses;
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/BatchRangeHelper.java	Fri Dec 21 21:20:26 2012 +0100
@@ -0,0 +1,138 @@
+/*
+ * 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.util.Arrays;
+import java.util.NoSuchElementException;
+
+/**
+ * Helper to break given range [start..end] (inclusive bounds) to series of ranges,
+ * all but last are of batchSize length, and the last one is at most of batchSize+batchSizeTolerance length.
+ * 
+ * Range is [{@link #start()rangeStart}..{@link #end() rangeEnd}], where rangeStart is less or equal to rangeEnd.
+ * 
+ * When reverse range iteration is requested, original range is iterated from end to start, but the subranges 
+ * boundaries are in natural order. i.e. for 0..100, 10 first subrange would be [91..100], not [100..91]. This 
+ * helps clients of this class to get [start()..end()] in natural order regardless of iteration direction.
+ * 
+ * Note, this class (and its treatment of inclusive boundaries) is designed solely for use with methods that navigate
+ * revlogs and take (start,end) pair of inclusive range.
+ *  
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class BatchRangeHelper {
+
+	private final int rangeCount;
+	private final int rangeDelta;
+	private final int nextValueDelta;
+	private final int firstBoundary, lastBoundary;
+	private int rangeIndex, rangeValue, rangeStart, rangeEnd;
+
+	public BatchRangeHelper(int start, int end, int batchSize, boolean reverse) {
+		this(start, end, batchSize, batchSize/5, reverse); 
+	}
+
+	public BatchRangeHelper(int start, int end, int batchSize, int batchSizeTolerance, boolean reverse) {
+		assert end >= start;
+		assert start >= 0;
+		assert batchSize > 0;
+		assert batchSizeTolerance >= 0;
+		final int totalElements = end-start+1;
+		int batchRangeCount = totalElements / batchSize;
+		// batchRangeCount == 0, totalElements > 0 => need at least 1 range
+		if (batchRangeCount == 0 || batchRangeCount*batchSize+batchSizeTolerance < totalElements) {
+			batchRangeCount++;
+		}
+		rangeCount = batchRangeCount;
+		rangeDelta = batchSize-1; // ranges are inclusive, and always grow naturally.
+		nextValueDelta = reverse ? -batchSize : batchSize;
+		firstBoundary = reverse ? end-rangeDelta : start;
+		lastBoundary = reverse ? start : end;
+		reset();
+	}
+
+	public boolean hasNext() {
+		return rangeIndex < rangeCount;
+	}
+	
+	public void next() {
+		if (!hasNext()) {
+			throw new NoSuchElementException();
+		}
+		rangeStart = rangeValue;
+		rangeEnd = rangeValue + rangeDelta;
+		rangeValue += nextValueDelta;
+		if (++rangeIndex >= rangeCount) {
+			if (nextValueDelta < 0) {
+				// reverse iteration, lastBoundary represents start
+				rangeStart = lastBoundary;
+			} else {
+				// lastBoundary represents end
+				rangeEnd = lastBoundary;
+			}
+		}
+	}
+	
+	public int start() {
+		return rangeStart;
+	}
+	
+	public int end() {
+		return rangeEnd;
+	}
+	
+	public BatchRangeHelper reset() {
+		rangeValue = firstBoundary;
+		rangeIndex = 0;
+		return this;
+	}
+
+	public int[] toArray() {
+		int[] rv = new int[rangeCount*2];
+		reset();
+		int i = 0;
+		while (hasNext()) {
+			next();
+			rv[i++] = start();
+			rv[i++] = end();
+		}
+		reset();
+		return rv;
+	}
+
+	public static void main(String[] args) {
+		System.out.println("With remainder within tolerance");
+		System.out.println(Arrays.toString(new BatchRangeHelper(0, 102, 10, 4, false).toArray()));
+		System.out.println(Arrays.toString(new BatchRangeHelper(0, 102, 10, 4, true).toArray()));
+		System.out.println("With remainder out of tolerance");
+		System.out.println(Arrays.toString(new BatchRangeHelper(0, 102, 10, 2, false).toArray()));
+		System.out.println(Arrays.toString(new BatchRangeHelper(0, 102, 10, 2, true).toArray()));
+		System.out.println("Range smaller than batch");
+		System.out.println(Arrays.toString(new BatchRangeHelper(1, 9, 10, false).toArray()));
+		System.out.println(Arrays.toString(new BatchRangeHelper(1, 9, 10, true).toArray()));
+		System.out.println("Range smaller than batch and smaller than tolerance");
+		System.out.println(Arrays.toString(new BatchRangeHelper(1, 9, 10, 20, false).toArray()));
+		System.out.println(Arrays.toString(new BatchRangeHelper(1, 9, 10, 20, true).toArray()));
+		System.out.println("Zero tolerance");
+		System.out.println(Arrays.toString(new BatchRangeHelper(0, 100, 10, 0, false).toArray()));
+		System.out.println(Arrays.toString(new BatchRangeHelper(0, 100, 10, 0, true).toArray()));
+		System.out.println("Right to boundary");
+		System.out.println(Arrays.toString(new BatchRangeHelper(1, 100, 10, false).toArray()));
+		System.out.println(Arrays.toString(new BatchRangeHelper(1, 100, 10, true).toArray()));
+	}
+}
--- a/src/org/tmatesoft/hg/internal/IterateControlMediator.java	Thu Dec 20 20:21:59 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/IterateControlMediator.java	Fri Dec 21 21:20:26 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
@@ -28,7 +28,8 @@
 public class IterateControlMediator {
 
 	private final CancelSupport src;
-	private Callback receiver;
+	private final Callback receiver;
+	private CancelledException cancellation;
 
 	public IterateControlMediator(CancelSupport source, Lifecycle.Callback target) {
 		assert target != null;
@@ -40,11 +41,15 @@
 		if (src == null) {
 			return false;
 		}
+		if (cancellation != null) {
+			return true;
+		}
 		try {
 			src.checkCancelled();
 			return false;
 		} catch (CancelledException ex) {
 			receiver.stop();
+			cancellation = ex;
 			return true;
 		}
 	}
@@ -52,4 +57,8 @@
 	public void stop() {
 		receiver.stop();
 	}
+	
+	public CancelledException getCancelException() {
+		return cancellation;
+	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/LifecycleBridge.java	Fri Dec 21 21:20:26 2012 +0100
@@ -0,0 +1,92 @@
+/*
+ * 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 org.tmatesoft.hg.util.CancelSupport;
+import org.tmatesoft.hg.util.CancelledException;
+import org.tmatesoft.hg.util.ProgressSupport;
+
+/**
+ * Bridge low-level life-cycle ({@link Lifecycle}) API with high-level one ({@link ProgressSupport} and {@link CancelSupport}).
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class LifecycleBridge implements Lifecycle {
+	private final ProgressSupport progressHelper;
+	private final CancelSupport cancelSupport;
+	// may be null unless #start() is invoked
+	private Callback receiver;
+	private CancelledException cancellation;
+
+	
+	public LifecycleBridge(ProgressSupport progress, CancelSupport cancel) {
+		progressHelper = progress;
+		cancelSupport = cancel;
+	}
+
+	public void start(int count, Callback callback, Object token) {
+		receiver = callback;
+		if (progressHelper != null) {
+			progressHelper.start(count);
+		}
+	}
+
+	public void finish(Object token) {
+		if (progressHelper != null) {
+			progressHelper.done();
+		}
+		receiver = null;
+	}
+
+	// XXX SHALL work without start/finish sequence because
+	// HgLogCommand invokes ChangesetTransformer#next directly (i.e. not from
+	// inside a library's #range() or similar) to process changesets in unnatural order.
+	public void nextStep() {
+		if (progressHelper != null) {
+			progressHelper.worked(1);
+		}
+		if (cancelSupport == null) {
+			return;
+		}
+		try { 
+			cancelSupport.checkCancelled();
+		} catch (CancelledException ex) {
+			if (receiver != null) {
+				receiver.stop();
+			}
+			cancellation = ex;
+		}
+	}
+
+	public void stop() {
+		if (receiver != null) {
+			receiver.stop();
+		}
+	}
+	
+	/**
+	 * @return <code>true</code> iff {@link CancelledException} was thrown and catched. Forced stop doesn't count
+	 */
+	public boolean isCancelled() {
+		return cancellation != null;
+	}
+	
+	public CancelledException getCancelOrigin() {
+		return cancellation;
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/LifecycleProxy.java	Fri Dec 21 21:20:26 2012 +0100
@@ -0,0 +1,64 @@
+/*
+ * 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 org.tmatesoft.hg.util.Adaptable;
+
+/**
+ * Save callback and delegate to another lifecycle instance, if any
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class LifecycleProxy implements Lifecycle {
+	
+	private Callback lifecycleCallback;
+	private Lifecycle target;
+	
+	public LifecycleProxy() {
+	}
+
+	public LifecycleProxy(Object delegate) {
+		init(delegate);
+	}
+	
+
+	public void start(int count, Callback callback, Object token) {
+		lifecycleCallback = callback;
+		if (target != null) {
+			target.start(count, callback, token);
+		}
+	}
+
+	public void finish(Object token) {
+		if (target != null) {
+			target.finish(token);
+		}
+		lifecycleCallback = null;
+	}
+
+	public void init(Object delegate) {
+		target = Adaptable.Factory.getAdapter(delegate, Lifecycle.class, null);
+	}
+
+	public void stop() {
+		assert lifecycleCallback != null;
+		if (lifecycleCallback != null) {
+			lifecycleCallback.stop();
+		}
+	}
+}
\ No newline at end of file
--- a/src/org/tmatesoft/hg/internal/RevlogStream.java	Thu Dec 20 20:21:59 2012 +0100
+++ b/src/org/tmatesoft/hg/internal/RevlogStream.java	Fri Dec 21 21:20:26 2012 +0100
@@ -29,6 +29,7 @@
 import org.tmatesoft.hg.repo.HgInvalidRevisionException;
 import org.tmatesoft.hg.repo.HgInvalidStateException;
 import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.util.Adaptable;
 
 
 /**
@@ -382,6 +383,7 @@
 		private final boolean needData;
 		private DataAccess daIndex = null, daData = null;
 		private Lifecycle.BasicCallback cb = null;
+		private Lifecycle lifecycleListener = null;
 		private int lastRevisionRead = BAD_REVISION;
 		private DataAccess lastUserData;
 		// next are to track two major bottlenecks - patch application and actual time spent in inspector 
@@ -399,28 +401,35 @@
 			if (needData && !inline) {
 				daData = getDataStream();
 			}
-			if (inspector instanceof Lifecycle) {
+			lifecycleListener = Adaptable.Factory.getAdapter(inspector, Lifecycle.class, null);
+			if (lifecycleListener != null) {
 				cb = new Lifecycle.BasicCallback();
-				((Lifecycle) inspector).start(totalWork, cb, cb);
+				lifecycleListener.start(totalWork, cb, cb);
 			}
 //			applyTime = inspectorTime = 0; // TIMING
 		}
 
+		// invoked only once per instance
 		public void finish() {
 			if (lastUserData != null) {
 				lastUserData.done();
 				lastUserData = null;
 			}
-			if (inspector instanceof Lifecycle) {
-				((Lifecycle) inspector).finish(cb);
+			if (lifecycleListener != null) {
+				lifecycleListener.finish(cb);
+				lifecycleListener = null;
+				cb = null;
+				
 			}
 			daIndex.done();
 			if (daData != null) {
 				daData.done();
+				daData = null;
 			}
 //			System.out.printf("applyTime:%d ms, inspectorTime: %d ms\n", applyTime, inspectorTime); // TIMING
 		}
 
+		// may be invoked few times per instance life
 		public boolean range(int start, int end) throws IOException {
 			byte[] nodeidBuf = new byte[20];
 			int i;
--- a/src/org/tmatesoft/hg/repo/HgChangelog.java	Thu Dec 20 20:21:59 2012 +0100
+++ b/src/org/tmatesoft/hg/repo/HgChangelog.java	Fri Dec 21 21:20:26 2012 +0100
@@ -33,10 +33,11 @@
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.internal.Callback;
 import org.tmatesoft.hg.internal.DataAccess;
-import org.tmatesoft.hg.internal.IterateControlMediator;
 import org.tmatesoft.hg.internal.Lifecycle;
+import org.tmatesoft.hg.internal.LifecycleBridge;
 import org.tmatesoft.hg.internal.Pool;
 import org.tmatesoft.hg.internal.RevlogStream;
+import org.tmatesoft.hg.util.Adaptable;
 import org.tmatesoft.hg.util.CancelSupport;
 import org.tmatesoft.hg.util.ProgressSupport;
 
@@ -357,19 +358,33 @@
 		}
 	}
 
-	private static class RawCsetParser implements RevlogStream.Inspector, Lifecycle {
+	private static class RawCsetParser implements RevlogStream.Inspector, Adaptable {
 		
 		private final Inspector inspector;
 		private final Pool<String> usersPool;
 		private final RawChangeset cset = new RawChangeset();
-		private final ProgressSupport progressHelper;
-		private IterateControlMediator iterateControl;
+		// non-null when inspector uses high-level lifecycle entities (progress and/or cancel supports)
+		private final LifecycleBridge lifecycleStub;
+		// non-null when inspector relies on low-level lifecycle and is responsible
+		// to proceed any possible high-level entities himself.
+		private final Lifecycle inspectorLifecycle;
 
 		public RawCsetParser(HgChangelog.Inspector delegate) {
 			assert delegate != null;
 			inspector = delegate;
 			usersPool = new Pool<String>();
-			progressHelper = ProgressSupport.Factory.get(delegate);
+			inspectorLifecycle = Adaptable.Factory.getAdapter(delegate, Lifecycle.class, null);
+			if (inspectorLifecycle == null) {
+				ProgressSupport ph = Adaptable.Factory.getAdapter(delegate, ProgressSupport.class, null);
+				CancelSupport cs = Adaptable.Factory.getAdapter(delegate, CancelSupport.class, null);
+				if (cs != null || ph != null) {
+					lifecycleStub = new LifecycleBridge(ph, cs);
+				} else {
+					lifecycleStub = null;
+				}
+			} else {
+				lifecycleStub = null;
+			}
 		}
 
 		public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) {
@@ -378,26 +393,30 @@
 				cset.init(data, 0, data.length, usersPool);
 				// XXX there's no guarantee for Changeset.Callback that distinct instance comes each time, consider instance reuse
 				inspector.next(revisionNumber, Nodeid.fromBinary(nodeid, 0), cset);
-				progressHelper.worked(1);
+				if (lifecycleStub != null) {
+					lifecycleStub.nextStep();
+				}
 			} catch (HgInvalidDataFormatException ex) {
 				throw ex.setRevisionIndex(revisionNumber);  
 			} catch (IOException ex) {
 				// XXX need better exception, perhaps smth like HgChangelogException (extends HgInvalidControlFileException)
 				throw new HgInvalidControlFileException("Failed reading changelog", ex, null).setRevisionIndex(revisionNumber);  
 			}
-			if (iterateControl != null) {
-				iterateControl.checkCancelled();
+		}
+		
+		public <T> T getAdapter(Class<T> adapterClass) {
+			if (adapterClass == Lifecycle.class) {
+				if (inspectorLifecycle != null) {
+					return adapterClass.cast(inspectorLifecycle);
+				}
+				// reveal interest in lifecycle only when either progress or cancel support is there
+				// and inspector itself doesn't respond to lifecycle request
+				// lifecycleStub may still be null here (no progress and cancel), it's ok to cast(null) 
+				return adapterClass.cast(lifecycleStub);
+				
 			}
+			return Adaptable.Factory.getAdapter(inspector, adapterClass, null);
 		}
 
-		public void start(int count, Callback callback, Object token) {
-			CancelSupport cs = CancelSupport.Factory.get(inspector, null);
-			iterateControl = cs == null ? null : new IterateControlMediator(cs, callback);
-			progressHelper.start(count);
-		}
-
-		public void finish(Object token) {
-			progressHelper.done();
-		}
 	}
 }
--- a/test/org/tmatesoft/hg/test/ErrorCollectorExt.java	Thu Dec 20 20:21:59 2012 +0100
+++ b/test/org/tmatesoft/hg/test/ErrorCollectorExt.java	Fri Dec 21 21:20:26 2012 +0100
@@ -59,7 +59,7 @@
 	public void assertTrue(final boolean value) {
 		assertTrue(null, value);
 	}
-
+	
 	public void assertTrue(final String reason, final boolean value) {
 		checkSucceeds(new Callable<Object>() {
 			public Object call() throws Exception {
@@ -72,4 +72,12 @@
 	public <T> void assertEquals(T expected, T actual) {
 		checkThat(null, actual, CoreMatchers.equalTo(expected));
 	}
+
+	public <T> void assertEquals(String reason, T expected, T actual) {
+		checkThat(reason, actual, CoreMatchers.equalTo(expected));
+	}
+
+	public void fail(String reason) {
+		addError(new AssertionError(reason));
+	}
 }
\ No newline at end of file
--- a/test/org/tmatesoft/hg/test/TestAuxUtilities.java	Thu Dec 20 20:21:59 2012 +0100
+++ b/test/org/tmatesoft/hg/test/TestAuxUtilities.java	Fri Dec 21 21:20:26 2012 +0100
@@ -147,83 +147,81 @@
 			}
 		}
 	}
+	
+	static class CancelAtValue {
+		public int lastSeen;
+		public final int stopValue;
+		protected final CancelImpl cancelImpl = new CancelImpl();
+
+		protected CancelAtValue(int value) {
+			stopValue = value;
+		}
+		
+		protected void nextValue(int value) {
+			lastSeen = value;
+			if (value == stopValue) {
+				cancelImpl.stop();
+			}
+		}
+	}
 
 	@Test
 	public void testChangelogCancelSupport() throws Exception {
 		HgRepository repository = Configuration.get().find("branches-1"); // any repo with more revisions
-		class InspectorImplementsCancel implements HgChangelog.Inspector, CancelSupport {
-			public final int when2stop;
-			public int lastVisitet = 0;
-			private final CancelImpl cancelImpl = new CancelImpl(); 
+		class InspectorImplementsCancel extends CancelAtValue implements HgChangelog.Inspector, CancelSupport {
 
 			public InspectorImplementsCancel(int limit) {
-				when2stop = limit;
+				super(limit);
 			}
 			
 			public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
-				lastVisitet = revisionNumber;
-				if (revisionNumber == when2stop) {
-					cancelImpl.stop();
-				}
+				nextValue(revisionNumber);
 			}
 
 			public void checkCancelled() throws CancelledException {
 				cancelImpl.checkCancelled();
 			}
 		};
-		class InspectorImplementsAdaptable implements HgChangelog.Inspector, Adaptable {
-			public final int when2stop;
-			public int lastVisitet = 0;
-			private final CancelImpl cancelImpl = new CancelImpl();
-			
+		class InspectorImplementsAdaptable extends CancelAtValue implements HgChangelog.Inspector, Adaptable {
 			public InspectorImplementsAdaptable(int limit) {
-				when2stop = limit;
+				super(limit);
 			}
 			
 			public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
-				lastVisitet = revisionNumber;
-				if (revisionNumber == when2stop) {
-					cancelImpl.stop();
-				}
+				nextValue(revisionNumber);
 			}
+
 			public <T> T getAdapter(Class<T> adapterClass) {
 				if (CancelSupport.class == adapterClass) {
 					return adapterClass.cast(cancelImpl);
 				}
 				return null;
 			}
-			
 		}
 		//
 		InspectorImplementsCancel insp1;
 		repository.getChangelog().all(insp1= new InspectorImplementsCancel(2));
-		Assert.assertEquals(insp1.when2stop, insp1.lastVisitet);
+		Assert.assertEquals(insp1.stopValue, insp1.lastSeen);
 		repository.getChangelog().all(insp1 = new InspectorImplementsCancel(12));
-		Assert.assertEquals(insp1.when2stop, insp1.lastVisitet);
+		Assert.assertEquals(insp1.stopValue, insp1.lastSeen);
 		//
 		InspectorImplementsAdaptable insp2;
 		repository.getChangelog().all(insp2= new InspectorImplementsAdaptable(3));
-		Assert.assertEquals(insp2.when2stop, insp2.lastVisitet);
+		Assert.assertEquals(insp2.stopValue, insp2.lastSeen);
 		repository.getChangelog().all(insp2 = new InspectorImplementsAdaptable(10));
-		Assert.assertEquals(insp2.when2stop, insp2.lastVisitet);
+		Assert.assertEquals(insp2.stopValue, insp2.lastSeen);
 	}
 	
 	@Test
 	public void testManifestCancelSupport() throws Exception {
 		HgRepository repository = Configuration.get().find("branches-1"); // any repo with as many revisions as possible
-		class InspectorImplementsAdaptable implements HgManifest.Inspector, Adaptable {
-			public final int when2stop;
-			public int lastVisitet = 0;
-			private final CancelImpl cancelImpl = new CancelImpl(); 
-
+		class InspectorImplementsAdaptable extends CancelAtValue implements HgManifest.Inspector, Adaptable {
 			public InspectorImplementsAdaptable(int limit) {
-				when2stop = limit;
+				super(limit);
 			}
 
 			public boolean begin(int mainfestRevision, Nodeid nid, int changelogRevision) {
-				if (++lastVisitet == when2stop) {
-					cancelImpl.stop();
-				}
+				nextValue(lastSeen+1);
 				return true;
 			}
 
@@ -244,9 +242,9 @@
 		}
 		InspectorImplementsAdaptable insp1;
 		repository.getManifest().walk(0, TIP, insp1= new InspectorImplementsAdaptable(3));
-		Assert.assertEquals(insp1.when2stop, insp1.lastVisitet);
+		Assert.assertEquals(insp1.stopValue, insp1.lastSeen);
 		repository.getManifest().walk(0, TIP, insp1 = new InspectorImplementsAdaptable(10));
-		Assert.assertEquals(insp1.when2stop, insp1.lastVisitet);
+		Assert.assertEquals(insp1.stopValue, insp1.lastSeen);
 	}
 	
 	@Test
--- a/test/org/tmatesoft/hg/test/TestHistory.java	Thu Dec 20 20:21:59 2012 +0100
+++ b/test/org/tmatesoft/hg/test/TestHistory.java	Fri Dec 21 21:20:26 2012 +0100
@@ -42,10 +42,13 @@
 import org.tmatesoft.hg.core.HgLogCommand;
 import org.tmatesoft.hg.core.HgLogCommand.CollectHandler;
 import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.internal.AdapterPlug;
 import org.tmatesoft.hg.repo.HgLookup;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.test.LogOutputParser.Record;
 import org.tmatesoft.hg.util.Adaptable;
+import org.tmatesoft.hg.util.CancelSupport;
+import org.tmatesoft.hg.util.CancelledException;
 import org.tmatesoft.hg.util.Pair;
 import org.tmatesoft.hg.util.Path;
 
@@ -93,7 +96,10 @@
 		changelogParser.reset();
 		eh.run("hg", "log", "--debug");
 		List<HgChangeset> r = new HgLogCommand(repo).execute();
-		report("hg log - COMPLETE REPO HISTORY", r, true); 
+		report("hg log - COMPLETE REPO HISTORY", r, true);
+		
+		r = new HgLogCommand(repo).debugSwitch1().execute();
+		report("hg log - COMPLETE REPO HISTORY, FROM NEW TO OLD", r, false);
 	}
 	
 	@Test
@@ -193,8 +199,21 @@
 		String lastRevOfFname1 = "369c0882d477c11424a62eb4b791e86d1d4b6769";
 		errorCollector.assertEquals(lastRevOfFname1, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
 		report("HgChangesetHandler(renames: true, ancestry:false)", h.getChanges(), true);
-		
-		// TODO direction
+		//
+		// Direction
+		h = new CollectWithRenameHandler();
+		new HgLogCommand(repo).file(fname2, true, false).debugSwitch1().execute(h);
+		// Identical rename shall be reported, at the same moment 
+		errorCollector.assertEquals(1, h.renames.size());
+		rename = h.renames.get(0);
+		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
+		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
+		errorCollector.assertEquals(1, h.lastChangesetReportedAtRename.size());
+		// new to old, recently reported would be the very first revision fname2 pops up
+		String firstRevOfFname2 = "27e7a69373b74d42e75f3211e56510ff17d01370";
+		errorCollector.assertEquals(firstRevOfFname2, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
+		report("HgChangesetHandler(renames: true, ancestry:false)", h.getChanges(), false);
+		//
 		// TODO TreeChangeHandler
 	}
 		
@@ -228,8 +247,12 @@
 		new HgLogCommand(repo).file(fname2, false, true).execute(h);
 		errorCollector.assertEquals(0, h.renames.size());
 		report("HgChangesetHandler(renames: false, ancestry:true)", h.getChanges(), fname2Follow, true, errorCollector);
-
-		// TODO direction
+		//
+		// Direction
+		h = new CollectWithRenameHandler();
+		new HgLogCommand(repo).file(fname2, false, true).debugSwitch1().execute(h);
+		report("HgChangesetHandler(renames: false, ancestry:true)", h.getChanges(), fname2Follow, false/*!!!*/, errorCollector);
+		//
 		// TODO TreeChangeHandler
 	}
 
@@ -256,10 +279,80 @@
 		errorCollector.assertEquals(fname1BranchRevision, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
 		// finally, match output
 		report("HgChangesetHandler(renames: true, ancestry:true)", h.getChanges(), true);
-		// TODO direction
+		//
+		// Switch direction and compare, order shall match that from console
+		h = new CollectWithRenameHandler();
+		new HgLogCommand(repo).file(fname2, true, true).debugSwitch1().execute(h);
+		// Identical rename event shall be reported
+		errorCollector.assertEquals(1, h.renames.size());
+		rename = h.renames.get(0);
+		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
+		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
+		// new to old, recently reported would be the very first revision fname2 pops up
+		String firstRevOfFname2 = "27e7a69373b74d42e75f3211e56510ff17d01370";
+		errorCollector.assertEquals(firstRevOfFname2, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
+		report("HgChangesetHandler(renames: true, ancestry:true)", h.getChanges(), false /*do not reorder console results !!!*/);
+		//
 		// TreeChangeHandler in #testChangesetTreeFollowRenameAndAncestry
 	}
 
+	/**
+	 * @see TestAuxUtilities#testChangelogCancelSupport()
+	 */
+	@Test
+	public void testLogCommandCancelSupport() throws Exception {
+		repo  = Configuration.get().find("branches-1"); // any repo with more revisions
+		class BaseCancel extends TestAuxUtilities.CancelAtValue implements HgChangesetHandler {
+			BaseCancel(int limit) {
+				super(limit);
+			}
+			public void cset(HgChangeset changeset) throws HgCallbackTargetException {
+				nextValue(changeset.getRevisionIndex());
+			}
+		};
+		class ImplementsCancel extends BaseCancel implements CancelSupport {
+			ImplementsCancel(int limit) {
+				super(limit);
+			}
+			public void checkCancelled() throws CancelledException {
+				cancelImpl.checkCancelled();
+			}
+		};
+		class AdaptsToCancel extends BaseCancel implements Adaptable {
+			AdaptsToCancel(int limit) {
+				super(limit);
+			}
+			public <T> T getAdapter(Class<T> adapterClass) {
+				if (adapterClass == CancelSupport.class) {
+					return adapterClass.cast(cancelImpl);
+				}
+				return null;
+			}
+		}
+
+		BaseCancel insp = new ImplementsCancel(3);
+		try {
+			new HgLogCommand(repo).execute(insp);
+			errorCollector.fail("CancelSupport as implemented iface");
+		} catch (CancelledException ex) {
+			errorCollector.assertEquals("CancelSupport as implemented iface", insp.stopValue, insp.lastSeen);
+		}
+		insp = new AdaptsToCancel(5);
+		try {
+			new HgLogCommand(repo).execute(insp);
+			errorCollector.fail("Adaptable to CancelSupport");
+		} catch (CancelledException ex) { 
+			errorCollector.assertEquals("Adaptable to CancelSupport", insp.stopValue, insp.lastSeen);
+		}
+		insp = new BaseCancel(9);
+		try {
+			new HgLogCommand(repo).set(insp.cancelImpl).execute(insp);
+			errorCollector.fail("cmd#set(CancelSupport)");
+		} catch (CancelledException e) {
+			errorCollector.assertEquals("cmd#set(CancelSupport)", insp.stopValue, insp.lastSeen);
+		}
+	}
+
 	private void report(String what, List<HgChangeset> r, boolean reverseConsoleResult) {
 		final List<Record> consoleResult = changelogParser.getResult();
 		report(what, r, consoleResult, reverseConsoleResult, errorCollector);
@@ -278,6 +371,8 @@
 				break;
 			}
 			Record cr = consoleResultItr.next();
+			// flags, not separate checkThat() because when lists are large, and do not match,
+			// number of failures may slow down test process significantly
 			int x = cs.getRevisionIndex() == cr.changesetIndex ? 0x1 : 0;
 			x |= cs.getDate().toString().equals(cr.date) ? 0x2 : 0;
 			x |= cs.getNodeid().toString().equals(cr.changesetNodeid) ? 0x4 : 0;
@@ -398,34 +493,6 @@
 
 	////
 	
-	private static class AdapterPlug implements Adaptable {
-		private final Map<Class<?>, Object> adapters = new HashMap<Class<?>, Object>();
-		private final List<Class<?>> adapterUses = new ArrayList<Class<?>>();
-		
-		public <T> void attachAdapter(Class<T> adapterClass, T instance) {
-			adapters.put(adapterClass, instance);
-		}
-
-		public <T> T getAdapter(Class<T> adapterClass) {
-			Object instance = adapters.get(adapterClass);
-			if (instance != null) {
-				adapterUses.add(adapterClass);
-				return adapterClass.cast(instance);
-			}
-			return null;
-		}
-		
-		public int getAdapterUse(Class<?> adapterClass) {
-			int uses = 0;
-			for (Class<?> c : adapterUses) {
-				if (c == adapterClass) {
-					uses++;
-				}
-			}
-			return uses;
-		}
-	}
-	
 	private final class TreeCollectHandler extends AdapterPlug implements HgChangesetTreeHandler {
 		private final LinkedList<HgChangeset> cmdResult = new LinkedList<HgChangeset>();
 		private final boolean reverseResult;