changeset 514:5dcb4581c8ef

Report renames when following file history tree with HgFileRenameHandlerMixin
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Mon, 17 Dec 2012 19:06:07 +0100
parents a41d955dc360
children e6c8b9b654b2
files cmdline/org/tmatesoft/hg/console/Log.java cmdline/org/tmatesoft/hg/console/Main.java src/org/tmatesoft/hg/core/HgChangesetHandler.java src/org/tmatesoft/hg/core/HgFileRenameHandlerMixin.java src/org/tmatesoft/hg/core/HgFileRevision.java src/org/tmatesoft/hg/core/HgLogCommand.java test/org/tmatesoft/hg/test/TestHistory.java
diffstat 7 files changed, 214 insertions(+), 68 deletions(-) [+]
line wrap: on
line diff
--- a/cmdline/org/tmatesoft/hg/console/Log.java	Mon Dec 17 15:01:57 2012 +0100
+++ b/cmdline/org/tmatesoft/hg/console/Log.java	Mon Dec 17 19:06:07 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
@@ -21,6 +21,7 @@
 import java.util.List;
 
 import org.tmatesoft.hg.core.HgChangesetHandler;
+import org.tmatesoft.hg.core.HgFileRenameHandlerMixin;
 import org.tmatesoft.hg.core.HgFileRevision;
 import org.tmatesoft.hg.core.HgLogCommand;
 import org.tmatesoft.hg.repo.HgDataFile;
@@ -121,12 +122,20 @@
 	}
 
 	private static final class Dump extends ChangesetDumpHandler implements HgChangesetHandler.WithCopyHistory {
+		private final RenameDumpHandler renameHandlerDelegate;
 
 		public Dump(HgRepository hgRepo) {
 			super(hgRepo);
+			renameHandlerDelegate = new RenameDumpHandler();
 		}
 		
 		public void copy(HgFileRevision from, HgFileRevision to) {
+			renameHandlerDelegate.copy(from, to);
+		}
+	}
+	
+	static class RenameDumpHandler implements HgFileRenameHandlerMixin {
+		public void copy(HgFileRevision from, HgFileRevision to) {
 			System.out.printf("Got notified that %s(%s) was originally known as %s(%s)\n", to.getPath(), to.getRevision(), from.getPath(), from.getRevision());
 		}
 	}
--- a/cmdline/org/tmatesoft/hg/console/Main.java	Mon Dec 17 15:01:57 2012 +0100
+++ b/cmdline/org/tmatesoft/hg/console/Main.java	Mon Dec 17 19:06:07 2012 +0100
@@ -30,6 +30,7 @@
 import org.tmatesoft.hg.core.HgChangeset;
 import org.tmatesoft.hg.core.HgChangesetTreeHandler;
 import org.tmatesoft.hg.core.HgException;
+import org.tmatesoft.hg.core.HgFileRenameHandlerMixin;
 import org.tmatesoft.hg.core.HgFileRevision;
 import org.tmatesoft.hg.core.HgLogCommand;
 import org.tmatesoft.hg.core.HgManifestCommand;
@@ -66,6 +67,7 @@
 import org.tmatesoft.hg.repo.ext.HgExtensionsManager;
 import org.tmatesoft.hg.repo.ext.HgExtensionsManager.HgExt;
 import org.tmatesoft.hg.repo.ext.Rebase;
+import org.tmatesoft.hg.util.Adaptable;
 import org.tmatesoft.hg.util.FileWalker;
 import org.tmatesoft.hg.util.LogFacility;
 import org.tmatesoft.hg.util.Pair;
@@ -174,7 +176,7 @@
 		HgLogCommand cmd = new HgLogCommand(hgRepo);
 		cmd.file("file1b.txt", true);
 		final int[] count = new int[] { 0 };
-		cmd.execute(new HgChangesetTreeHandler() {
+		class MyHandler implements HgChangesetTreeHandler, Adaptable {
 			public void treeElement(HgChangesetTreeHandler.TreeElement entry) {
 				StringBuilder sb = new StringBuilder();
 				HashSet<Nodeid> test = new HashSet<Nodeid>(entry.childRevisions());
@@ -211,7 +213,20 @@
 				}
 				count[0]++;
 			}
-		});
+
+			public <T> T getAdapter(Class<T> adapterClass) {
+				if (adapterClass == HgFileRenameHandlerMixin.class) {
+					// in fact, new instance is not very nice, however
+					// getAdapter callers are supposed to understand the risk of new instance
+					// and cache returned value
+					// besides, stateless implementation of RenameDumpHandler
+					// doesn't really care about few instances 
+					return adapterClass.cast(new Log.RenameDumpHandler());
+				}
+				return null;
+			}
+		};
+		cmd.execute(new MyHandler());
 		System.out.println(count[0]);
 		final long end = System.nanoTime();
 		System.out.printf("buildFileLog: %,d ms\n", (end-start)/1000);
--- a/src/org/tmatesoft/hg/core/HgChangesetHandler.java	Mon Dec 17 15:01:57 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgChangesetHandler.java	Mon Dec 17 19:06:07 2012 +0100
@@ -17,6 +17,7 @@
 package org.tmatesoft.hg.core;
 
 import org.tmatesoft.hg.internal.Callback;
+import org.tmatesoft.hg.util.Adaptable;
 import org.tmatesoft.hg.util.Path;
 
 /**
@@ -37,20 +38,17 @@
 
 	/**
 	 * When {@link HgLogCommand} is executed against file, handler passed to {@link HgLogCommand#execute(HgChangesetHandler)} may optionally
-	 * implement this interface to get information about file renames. Method {@link #copy(HgFileRevision, HgFileRevision)} would
-	 * get invoked prior any changeset of the original file (if file history being followed) is reported via {@link #cset(HgChangeset)}.
+	 * implement this interface (or make it available through {@link Adaptable#getAdapter(Class)} to get information about file renames. 
+	 * Method {@link #copy(HgFileRevision, HgFileRevision)} would get invoked prior any changeset of the original file 
+	 * (if file history being followed) is reported via {@link #cset(HgChangeset)}.
 	 * 
 	 * For {@link HgLogCommand#file(Path, boolean)} with renamed file path and follow argument set to false, 
 	 * {@link #copy(HgFileRevision, HgFileRevision)} would be invoked for the first copy/rename in the history of the file, but not 
 	 * followed by any changesets. 
+	 * 
+	 * @see HgFileRenameHandlerMixin
 	 */
 	@Callback
-	public interface WithCopyHistory extends HgChangesetHandler {
-		// XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them?
-
-		/**
-		 * @throws HgCallbackTargetException wrapper object for any exception user code may produce 
-		 */
-		void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException;
+	public interface WithCopyHistory extends HgChangesetHandler, HgFileRenameHandlerMixin {
 	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/core/HgFileRenameHandlerMixin.java	Mon Dec 17 19:06:07 2012 +0100
@@ -0,0 +1,39 @@
+/*
+ * 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.core;
+
+import org.tmatesoft.hg.util.Adaptable;
+
+/**
+ * Addition to file history handlers (like {@link HgChangesetHandler} and {@link HgChangesetTreeHandler}) 
+ * to receive notification about rename in the history of the file being walked.
+ * 
+ * This mix-in shall be available from the host handler through the {@link Adaptable} mechanism, see
+ * {@link Adaptable.Factory#getAdapter(Object, Class, Object)}. Hence, implementing 
+ * this interface in addition to host's would be the easiest way to achieve that.
+ *  
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public interface HgFileRenameHandlerMixin {
+	// XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them?
+
+	/**
+	 * @throws HgCallbackTargetException wrapper object for any exception user code may produce 
+	 */
+	void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException;
+}
--- a/src/org/tmatesoft/hg/core/HgFileRevision.java	Mon Dec 17 15:01:57 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgFileRevision.java	Mon Dec 17 19:06:07 2012 +0100
@@ -69,10 +69,18 @@
 		origin = orig; 
 	}
 	
+	public HgFileRevision(HgDataFile fileNode, Nodeid fileRevision, Path origin) {
+		this(fileNode.getRepo(), fileRevision, null, fileNode.getPath(), origin); 
+	}
+	
 	public Path getPath() {
 		return path;
 	}
 
+	/**
+	 * Revision of the file
+	 * @return never <code>null</code>
+	 */
 	public Nodeid getRevision() {
 		return revision;
 	}
--- a/src/org/tmatesoft/hg/core/HgLogCommand.java	Mon Dec 17 15:01:57 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgLogCommand.java	Mon Dec 17 19:06:07 2012 +0100
@@ -45,6 +45,7 @@
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgRuntimeException;
 import org.tmatesoft.hg.repo.HgStatusCollector;
+import org.tmatesoft.hg.util.Adaptable;
 import org.tmatesoft.hg.util.CancelSupport;
 import org.tmatesoft.hg.util.CancelledException;
 import org.tmatesoft.hg.util.Pair;
@@ -73,7 +74,17 @@
 	private int startRev = 0, endRev = TIP;
 	private Calendar date;
 	private Path file;
-	private boolean followHistory; // makes sense only when file != null
+	/*
+	 * Whether to iterate file origins, if any.
+	 * Makes sense only when file != null
+	 */
+	private boolean followRenames;
+	/*
+	 * Whether to track history of the selected file version (based on file revision
+	 * in working dir parent), follow ancestors only.
+	 * Note, 'hg log --follow' combines both #followHistory and #followAncestry
+	 */
+	private boolean followAncestry;
 	private ChangesetTransformer csetTransform;
 	private HgParentChildMap<HgChangelog> parentHelper;
 	
@@ -184,7 +195,7 @@
 	public HgLogCommand file(Path file, boolean followCopyRename) {
 		// multiple? Bad idea, would need to include extra method into Handler to tell start of next file
 		this.file = file;
-		followHistory = followCopyRename;
+		followRenames = followAncestry = followCopyRename;
 		return this;
 	}
 	
@@ -256,24 +267,25 @@
 				// FIXME startRev and endRev ARE CHANGESET REVISIONS, not that of FILE!!!
 				fileNode.history(startRev, endRev, this);
 				csetTransform.checkFailure();
+				final HgFileRenameHandlerMixin withCopyHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
 				if (fileNode.isCopy()) {
 					// even if we do not follow history, report file rename
 					do {
-						if (handler instanceof HgChangesetHandler.WithCopyHistory) {
+						if (withCopyHandler != null) {
 							HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), null, fileNode.getCopySourceName());
 							HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), null, fileNode.getPath(), src.getPath());
-							((HgChangesetHandler.WithCopyHistory) handler).copy(src, dst);
+							withCopyHandler.copy(src, dst);
 						}
 						if (limit > 0 && count >= limit) {
 							// if limit reach, follow is useless.
 							break;
 						}
-						if (followHistory) {
+						if (followRenames) {
 							fileNode = repo.getFileNode(fileNode.getCopySourceName());
 							fileNode.history(this);
 							csetTransform.checkFailure();
 						}
-					} while (followHistory && fileNode.isCopy());
+					} while (followRenames && fileNode.isCopy());
 				}
 			}
 		} catch (HgRuntimeException ex) {
@@ -306,44 +318,29 @@
 		}
 		final ProgressSupport progressHelper = getProgressSupport(handler);
 		final CancelSupport cancelHelper = getCancelSupport(handler, true);
+		final HgFileRenameHandlerMixin renameHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
 
 		// builds tree of nodes according to parents in file's revlog
-		final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followHistory);
+		final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followRenames);
 		// we iterate separate histories of each filename, need to connect
 		// last node of historyA with first node of historyB (A renamed to B case)
 		// to make overall history smooth.
 		HistoryNode lastFromPrevIteration = null;
+		HgFileRevision copiedFrom = null, copiedTo = null;
+		boolean shallReportRenameAfter1Step = false;
 		
 		final int CACHE_CSET_IN_ADVANCE_THRESHOLD = 100; /* XXX is it really worth it? */
 		ElementImpl ei = null;
 
 		// renamed files in the queue are placed with respect to #iterateDirection
 		// i.e. if we iterate from new to old, recent filenames come first
-		LinkedList<Pair<HgDataFile, Nodeid>> fileRenamesQueue = buildFileRenamesQueue();
+		List<Pair<HgDataFile, Nodeid>> fileRenamesQueue = buildFileRenamesQueue();
 		progressHelper.start(4 * fileRenamesQueue.size());
-		do {
+		for (int namesIndex = 0, renamesQueueSize = fileRenamesQueue.size(); namesIndex < renamesQueueSize; namesIndex++) {
  
-			Pair<HgDataFile, Nodeid> renameInfo = fileRenamesQueue.removeFirst();
+			final Pair<HgDataFile, Nodeid> renameInfo = fileRenamesQueue.get(namesIndex);
 			cancelHelper.checkCancelled();
-			HgDataFile fileNode = renameInfo.first();
-			Nodeid fileLastRevToVisit = null;
-			if (followHistory) {
-				fileLastRevToVisit = renameInfo.second();
-				if (fileLastRevToVisit == null) {
-					// it's either first or last item in the queue, depending on iteration order
-					assert fileRenamesQueue.isEmpty() || /*awful way to find out it's first iteration*/ lastFromPrevIteration == null;
-					// TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex)
-					// or in the HgDataFile (getWorkingCopyOriginRevision)
-					Nodeid wdParentChangeset = repo.getWorkingCopyParents().first();
-					if (!wdParentChangeset.isNull()) {
-						int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset);
-						fileLastRevToVisit = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath());
-					}
-					// else fall-through, assume lastRevision() is ok here
-				}
-			}
-			int fileLastRevIndexToVisit = fileLastRevToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevToVisit);
-			final List<HistoryNode> changeHistory = treeBuildInspector.go(fileNode, fileLastRevIndexToVisit);
+			final List<HistoryNode> changeHistory = treeBuildInspector.go(renameInfo.first(), renameInfo.second());
 			assert changeHistory.size() > 0;
 			progressHelper.worked(1);
 			cancelHelper.checkCancelled();
@@ -370,25 +367,43 @@
 					// forward, from old to new:
 					// A(0..n) -> B(0..m). First, report A(0)..A(n-1)
 					// then A(n).bind(B(0))
-					HistoryNode oldestOfTheNextChunk = changeHistory.get(0);
-					lastFromPrevIteration.bindChild(oldestOfTheNextChunk);
+					HistoryNode oldestOfTheNextChunk = changeHistory.get(0); // B(0)
+					lastFromPrevIteration.bindChild(oldestOfTheNextChunk); // lastFromPrevIteration is A(n)
 					changeHistory.add(0, lastFromPrevIteration);
+					if (renameHandler != null) { // shall report renames
+						assert namesIndex > 0;
+						HgDataFile lastIterationFileNode = fileRenamesQueue.get(namesIndex-1).first(); // A
+						copiedFrom = new HgFileRevision(lastIterationFileNode, lastFromPrevIteration.fileRevision, null);
+						copiedTo = new HgFileRevision(renameInfo.first(), oldestOfTheNextChunk.fileRevision, copiedFrom.getPath());
+						shallReportRenameAfter1Step = true; // report rename after A(n)
+					}
 				} else {
 					assert iterateDirection == IterateDirection.FromNewToOld;
 					// A renamed to B. A(0..n) -> B(0..m). 
-					// First, report B(m), B(m-1)...B(1), then A(n).bind(B(0))
+					// First, report B(m), B(m-1)...B(1), then A(n).bind(B(0)), report B(0), A(n)...
 					HistoryNode newestOfNextChunk = changeHistory.get(changeHistory.size() - 1); // A(n)
 					newestOfNextChunk.bindChild(lastFromPrevIteration);
 					changeHistory.add(lastFromPrevIteration);
+					if (renameHandler != null) {
+						assert namesIndex > 0;
+						// renameInfo points to chunk of name A now, and lastFromPrevIteration (from namesIndex-1) is B
+						copiedFrom = new HgFileRevision(renameInfo.first(), newestOfNextChunk.fileRevision, null);
+						HgDataFile lastIterationFileNode = fileRenamesQueue.get(namesIndex-1).first(); // B
+						copiedTo = new HgFileRevision(lastIterationFileNode, lastFromPrevIteration.fileRevision, copiedFrom.getPath());
+						shallReportRenameAfter1Step = true; // report rename after B(0)
+					}
 				}
 			}
-			if (!fileRenamesQueue.isEmpty()) {
+			if (namesIndex + 1 < renamesQueueSize) {
+				// there's at least one more name we are going to look at, save
+				// one element for later binding
+				//
 				if (iterateDirection == IterateDirection.FromOldToNew) {
 					// save newest, and exclude it from this iteration (postpone for next)
 					lastFromPrevIteration = changeHistory.remove(changeHistory.size()-1);
 				} else {
 					assert iterateDirection == IterateDirection.FromNewToOld;
-					// save oldest, and exclude it from thi iteration (postpone for next)
+					// save oldest, and exclude it from this iteration (postpone for next)
 					lastFromPrevIteration = changeHistory.remove(0);
 				}
 			} else {
@@ -407,8 +422,16 @@
 				handler.treeElement(ei.init(n));
 				ph2.worked(1);
 				cancelHelper.checkCancelled();
+				if (shallReportRenameAfter1Step) {
+					assert renameHandler != null;
+					assert copiedFrom != null;
+					assert copiedTo != null;
+					renameHandler.copy(copiedFrom, copiedTo);
+					shallReportRenameAfter1Step = false;
+					copiedFrom = copiedTo = null;
+				}
 			}
-		} while (!fileRenamesQueue.isEmpty());
+		} // for fileRenamesQueue;
 		progressHelper.done();
 	}
 	
@@ -436,27 +459,39 @@
 	 * Follows file renames and build a list of all corresponding file nodes and revisions they were 
 	 * copied/renamed/branched at (IOW, their latest revision to look at).
 	 *  
-	 * If {@link #followHistory} is <code>false</code>, the list contains one element only, 
+	 * If {@link #followRenames} is <code>false</code>, the list contains one element only, 
 	 * file node with the name of the file as it was specified by the user.
 	 * 
-	 * For the most recent file revision is null.
+	 * For the most recent file revision depends on {@link #followAncestry}, and is file revision from working copy parent
+	 * in it's true. <code>null</code> indicates file's TIP revision shall be used.
 	 * 
 	 * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair
 	 * and possibly reuse this functionality
 	 * 
 	 * @return list of file renames, ordered with respect to {@link #iterateDirection}
 	 */
-	private LinkedList<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() {
+	private List<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() {
 		LinkedList<Pair<HgDataFile, Nodeid>> rv = new LinkedList<Pair<HgDataFile, Nodeid>>();
-		if (!followHistory) {
-			rv.add(new Pair<HgDataFile, Nodeid>(repo.getFileNode(file), null));
+		Nodeid startRev = null;
+		HgDataFile fileNode = repo.getFileNode(file);
+		if (followAncestry) {
+			// TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex)
+			// or in the HgDataFile (getWorkingCopyOriginRevision)
+			Nodeid wdParentChangeset = repo.getWorkingCopyParents().first();
+			if (!wdParentChangeset.isNull()) {
+				int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset);
+				startRev = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath());
+			}
+			// else fall-through, assume null (eventually, lastRevision()) is ok here
+		}
+		rv.add(new Pair<HgDataFile, Nodeid>(fileNode, startRev));
+		if (!followRenames) {
 			return rv;
 		}
-		Path fp = file;
-		Nodeid copyRev = null;
-		boolean isCopy;
-		do {
-			HgDataFile fileNode = repo.getFileNode(fp);
+		while (fileNode.isCopy()) {
+			Path fp = fileNode.getCopySourceName();
+			Nodeid copyRev = fileNode.getCopySourceRevision();
+			fileNode = repo.getFileNode(fp);
 			Pair<HgDataFile, Nodeid> p = new Pair<HgDataFile, Nodeid>(fileNode, copyRev);
 			if (iterateDirection == IterateDirection.FromOldToNew) {
 				rv.addFirst(p);
@@ -464,11 +499,7 @@
 				assert iterateDirection == IterateDirection.FromNewToOld;
 				rv.addLast(p);
 			}
-			if (isCopy = fileNode.isCopy()) {
-				fp = fileNode.getCopySourceName();
-				copyRev = fileNode.getCopySourceRevision();
-			}
-		} while (isCopy);
+		};
 		return rv;
 	}
 	
@@ -506,11 +537,12 @@
 		 * @return list of history elements, from oldest to newest. In case {@link #followAncestry} is <code>true</code>, the list
 		 * is modifiable (to further augment with last/first elements of renamed file histories)
 		 */
-		List<HistoryNode> go(HgDataFile fileNode, int lastRevisionIndex) throws HgInvalidControlFileException {
+		List<HistoryNode> go(HgDataFile fileNode, Nodeid fileLastRevisionToVisit) throws HgInvalidControlFileException {
 			resultHistory = null;
-			completeHistory = new HistoryNode[lastRevisionIndex+1];
+			int fileLastRevIndexToVisit = fileLastRevisionToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevisionToVisit);
+			completeHistory = new HistoryNode[fileLastRevIndexToVisit+1];
 			commitRevisions = new int[completeHistory.length];
-			fileNode.indexWalk(0, lastRevisionIndex, this);
+			fileNode.indexWalk(0, fileLastRevIndexToVisit, this);
 			if (!followAncestry) {
 				// in case when ancestor not followed, it's safe to return unmodifiable list
 				resultHistory = Arrays.asList(completeHistory);
@@ -539,7 +571,7 @@
 			LinkedList<HistoryNode> strippedHistoryList = new LinkedList<HistoryNode>();
 			LinkedList<HistoryNode> queue = new LinkedList<HistoryNode>();
 			// look for ancestors of the selected history node
-			queue.add(completeHistory[lastRevisionIndex]);
+			queue.add(completeHistory[fileLastRevIndexToVisit]);
 			do {
 				HistoryNode withFileChange = queue.removeFirst();
 				if (strippedHistoryList.contains(withFileChange)) {
--- a/test/org/tmatesoft/hg/test/TestHistory.java	Mon Dec 17 15:01:57 2012 +0100
+++ b/test/org/tmatesoft/hg/test/TestHistory.java	Mon Dec 17 19:06:07 2012 +0100
@@ -18,14 +18,17 @@
 
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -33,6 +36,7 @@
 import org.tmatesoft.hg.core.HgChangeset;
 import org.tmatesoft.hg.core.HgChangesetHandler;
 import org.tmatesoft.hg.core.HgChangesetTreeHandler;
+import org.tmatesoft.hg.core.HgFileRenameHandlerMixin;
 import org.tmatesoft.hg.core.HgFileRevision;
 import org.tmatesoft.hg.core.HgLogCommand;
 import org.tmatesoft.hg.core.HgLogCommand.CollectHandler;
@@ -40,6 +44,7 @@
 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.Pair;
 import org.tmatesoft.hg.util.Path;
 
@@ -142,12 +147,24 @@
 		final String fname = "file1_b";
 		assertTrue("[sanity]", repo.getFileNode(fname).exists());
 		eh.run("hg", "log", "--debug", "--follow", fname, "--cwd", repo.getLocation());
-		
+
+		final Map<Path,Path> renames = new HashMap<Path, Path>();
 		TreeCollectHandler h = new TreeCollectHandler(true);
+		h.attachAdapter(HgFileRenameHandlerMixin.class, new HgFileRenameHandlerMixin() {
+			
+			public void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException {
+				renames.put(from.getPath(), to.getPath());
+			}
+		});
 		h.checkPrevInParents = true;
 		new HgLogCommand(repo).file(fname, true).execute(h);
+
+		assertEquals(1, h.getAdapterUse(HgFileRenameHandlerMixin.class));
 		
 		report("execute with HgChangesetTreeHandler(follow == true)", h.getResult(), false);
+		
+		assertEquals(1, renames.size());
+		assertEquals(Path.create(fname), renames.get(Path.create("file1_a")));
 	}
 
 	private void report(String what, List<HgChangeset> r, boolean reverseConsoleResult) {
@@ -288,7 +305,35 @@
 
 	////
 	
-	private final class TreeCollectHandler implements HgChangesetTreeHandler {
+	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;
 		boolean checkPrevInChildren = false;