changeset 603:707b5c7c6fa4

Refactor HgBlameFacility: relevant action methods moved to proper home (HgDataFile), as facility doesn't provide anything but packaging of relevant methods/interfaces
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Mon, 06 May 2013 18:29:57 +0200
parents e3717fc7d26f
children c3505001a42a
files src/org/tmatesoft/hg/core/HgAnnotateCommand.java src/org/tmatesoft/hg/internal/BlameHelper.java src/org/tmatesoft/hg/internal/FileAnnotation.java src/org/tmatesoft/hg/repo/HgBlameFacility.java src/org/tmatesoft/hg/repo/HgBlameInspector.java src/org/tmatesoft/hg/repo/HgDataFile.java test/org/tmatesoft/hg/test/TestBlame.java
diffstat 7 files changed, 321 insertions(+), 352 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgAnnotateCommand.java	Mon May 06 17:11:29 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgAnnotateCommand.java	Mon May 06 18:29:57 2013 +0200
@@ -26,8 +26,7 @@
 import org.tmatesoft.hg.internal.FileAnnotation;
 import org.tmatesoft.hg.internal.FileAnnotation.LineDescriptor;
 import org.tmatesoft.hg.internal.FileAnnotation.LineInspector;
-import org.tmatesoft.hg.repo.HgBlameFacility.BlockData;
-import org.tmatesoft.hg.repo.HgBlameFacility;
+import org.tmatesoft.hg.repo.HgBlameInspector.BlockData;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.CancelSupport;
@@ -42,6 +41,7 @@
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
+ * @since 1.1
  */
 @Experimental(reason="Work in progress. Unstable API")
 public class HgAnnotateCommand extends HgAbstractCommand<HgAnnotateCommand> {
@@ -118,8 +118,7 @@
 		final int changesetStart = followRename ? 0 : df.getChangesetRevisionIndex(0);
 		Collector c = new Collector(cancellation);
 		FileAnnotation fa = new FileAnnotation(c);
-		HgBlameFacility af = new HgBlameFacility(df);
-		af.annotate(changesetStart, annotateRevision.get(), fa, HgIterateDirection.NewToOld);
+		df.annotate(changesetStart, annotateRevision.get(), fa, HgIterateDirection.NewToOld);
 		progress.worked(1);
 		c.throwIfCancelled();
 		cancellation.checkCancelled();
--- a/src/org/tmatesoft/hg/internal/BlameHelper.java	Mon May 06 17:11:29 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/BlameHelper.java	Mon May 06 18:29:57 2013 +0200
@@ -24,14 +24,13 @@
 import org.tmatesoft.hg.core.HgCallbackTargetException;
 import org.tmatesoft.hg.internal.DiffHelper.LineSequence;
 import org.tmatesoft.hg.internal.DiffHelper.LineSequence.ByteChain;
-import org.tmatesoft.hg.repo.HgBlameFacility.Block;
-import org.tmatesoft.hg.repo.HgBlameFacility.BlockData;
-import org.tmatesoft.hg.repo.HgBlameFacility.ChangeBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.EqualBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.Inspector;
-import org.tmatesoft.hg.repo.HgBlameFacility.RevisionDescriptor;
-import org.tmatesoft.hg.repo.HgBlameFacility.RevisionDescriptor.Recipient;
-import org.tmatesoft.hg.repo.HgBlameFacility;
+import org.tmatesoft.hg.repo.HgBlameInspector.Block;
+import org.tmatesoft.hg.repo.HgBlameInspector.BlockData;
+import org.tmatesoft.hg.repo.HgBlameInspector.ChangeBlock;
+import org.tmatesoft.hg.repo.HgBlameInspector.EqualBlock;
+import org.tmatesoft.hg.repo.HgBlameInspector.RevisionDescriptor;
+import org.tmatesoft.hg.repo.HgBlameInspector.RevisionDescriptor.Recipient;
+import org.tmatesoft.hg.repo.HgBlameInspector;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgInvalidStateException;
 import org.tmatesoft.hg.util.Adaptable;
@@ -40,18 +39,18 @@
 
 /**
  * Blame implementation
- * @see HgBlameFacility
+ * @see HgBlameInspector
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
 public class BlameHelper {
 	
-	private final Inspector insp;
+	private final HgBlameInspector insp;
 	private FileLinesCache linesCache;
 
 	// FIXME exposing internals (use of FileLinesCache through cons arg and #useFileUpTo) smells bad, refactor!
 
-	public BlameHelper(Inspector inspector, int cacheHint) {
+	public BlameHelper(HgBlameInspector inspector, int cacheHint) {
 		insp = inspector;
 		linesCache = new FileLinesCache(cacheHint);
 	}
@@ -192,7 +191,7 @@
 	}
 
 	private static class BlameBlockInspector extends DiffHelper.DeltaInspector<LineSequence> {
-		private final Inspector insp;
+		private final HgBlameInspector insp;
 		private final int csetOrigin;
 		private final int csetTarget;
 		private EqualBlocksCollector p2MergeCommon;
@@ -201,7 +200,7 @@
 		private final AnnotateRev annotatedRevision;
 		private HgCallbackTargetException error;
 
-		public BlameBlockInspector(HgDataFile df, int fileRevIndex, Inspector inspector, int originCset, int targetCset) {
+		public BlameBlockInspector(HgDataFile df, int fileRevIndex, HgBlameInspector inspector, int originCset, int targetCset) {
 			assert inspector != null;
 			insp = inspector;
 			annotatedRevision = new AnnotateRev();
--- a/src/org/tmatesoft/hg/internal/FileAnnotation.java	Mon May 06 17:11:29 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/FileAnnotation.java	Mon May 06 18:29:57 2013 +0200
@@ -19,15 +19,10 @@
 
 import org.tmatesoft.hg.core.HgCallbackTargetException;
 import org.tmatesoft.hg.core.HgIterateDirection;
-import org.tmatesoft.hg.repo.HgBlameFacility;
+import org.tmatesoft.hg.repo.HgBlameInspector;
+import org.tmatesoft.hg.repo.HgBlameInspector.RevisionDescriptor;
+import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgInvalidStateException;
-import org.tmatesoft.hg.repo.HgBlameFacility.AddBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.BlockData;
-import org.tmatesoft.hg.repo.HgBlameFacility.ChangeBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.DeleteBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.EqualBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.RevisionDescriptor;
-import org.tmatesoft.hg.repo.HgDataFile;
 
 /**
  * Produce output like 'hg annotate' does
@@ -35,7 +30,7 @@
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
-public class FileAnnotation implements HgBlameFacility.Inspector, RevisionDescriptor.Recipient {
+public class FileAnnotation implements HgBlameInspector, RevisionDescriptor.Recipient {
 
 	@Experimental(reason="The line-by-line inspector likely to become part of core/command API")
 	@Callback
@@ -58,8 +53,7 @@
 			return;
 		}
 		FileAnnotation fa = new FileAnnotation(insp);
-		HgBlameFacility af = new HgBlameFacility(df);
-		af.annotate(changelogRevisionIndex, fa, HgIterateDirection.NewToOld);
+		df.annotate(changelogRevisionIndex, fa, HgIterateDirection.NewToOld);
 	}
 
 	// keeps <startSeq1, startSeq2, len> of equal blocks, origin to target, from some previous step
--- a/src/org/tmatesoft/hg/repo/HgBlameFacility.java	Mon May 06 17:11:29 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,296 +0,0 @@
-/*
- * Copyright (c) 2013 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.repo;
-
-import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew;
-import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex;
-import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
-import static org.tmatesoft.hg.repo.HgRepository.TIP;
-
-import org.tmatesoft.hg.core.HgCallbackTargetException;
-import org.tmatesoft.hg.core.HgIterateDirection;
-import org.tmatesoft.hg.core.Nodeid;
-import org.tmatesoft.hg.internal.BlameHelper;
-import org.tmatesoft.hg.internal.Callback;
-import org.tmatesoft.hg.internal.Experimental;
-import org.tmatesoft.hg.internal.FileHistory;
-import org.tmatesoft.hg.internal.FileRevisionHistoryChunk;
-import org.tmatesoft.hg.util.Adaptable;
-
-/**
- * Facility with diff/annotate functionality.
- * 
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-@Experimental(reason="Unstable API")
-public final class HgBlameFacility {
-	private final HgDataFile df;
-	
-	public HgBlameFacility(HgDataFile file) {
-		if (file == null) {
-			throw new IllegalArgumentException();
-		}
-		df = file;
-	}
-	
-	/**
-	 * mimic 'hg diff -r clogRevIndex1 -r clogRevIndex2'
-	 */
-	public void diff(int clogRevIndex1, int clogRevIndex2, Inspector insp) throws HgCallbackTargetException {
-		// FIXME clogRevIndex1 and clogRevIndex2 may point to different files, need to decide whether to throw an exception
-		// or to attempt to look up correct file node (tricky)
-		int fileRevIndex1 = fileRevIndex(df, clogRevIndex1);
-		int fileRevIndex2 = fileRevIndex(df, clogRevIndex2);
-		BlameHelper bh = new BlameHelper(insp, 5);
-		bh.useFileUpTo(df, clogRevIndex2);
-		bh.diff(fileRevIndex1, clogRevIndex1, fileRevIndex2, clogRevIndex2);
-	}
-	
-	/**
-	 * Walk file history up/down to revision at given changeset and report changes for each revision
-	 */
-	public void annotate(int changelogRevisionIndex, Inspector insp, HgIterateDirection iterateOrder) throws HgCallbackTargetException {
-		annotate(0, changelogRevisionIndex, insp, iterateOrder);
-	}
-
-	/**
-	 * Walk file history range and report changes for each revision
-	 */
-	public void annotate(int changelogRevIndexStart, int changelogRevIndexEnd, Inspector insp, HgIterateDirection iterateOrder) throws HgCallbackTargetException {
-		if (wrongRevisionIndex(changelogRevIndexStart) || wrongRevisionIndex(changelogRevIndexEnd)) {
-			throw new IllegalArgumentException();
-		}
-		// Note, changelogRevIndexEnd may be TIP, while the code below doesn't tolerate constants
-		//
-		int lastRevision = df.getRepo().getChangelog().getLastRevision();
-		if (changelogRevIndexEnd == TIP) {
-			changelogRevIndexEnd = lastRevision;
-		}
-		HgInternals.checkRevlogRange(changelogRevIndexStart, changelogRevIndexEnd, lastRevision);
-		if (!df.exists()) {
-			return;
-		}
-		FileHistory fileHistory = new FileHistory(df, changelogRevIndexStart, changelogRevIndexEnd);
-		fileHistory.build();
-		BlameHelper bh = new BlameHelper(insp, 10);
-		for (FileRevisionHistoryChunk fhc : fileHistory.iterate(OldToNew)) {
-			// iteration order is not important here
-			bh.useFileUpTo(fhc.getFile(), fhc.getEndChangeset());
-		}
-		int[] fileClogParentRevs = new int[2];
-		int[] fileParentRevs = new int[2];
-		for (FileRevisionHistoryChunk fhc : fileHistory.iterate(iterateOrder)) {
-			for (int fri : fhc.fileRevisions(iterateOrder)) {
-				int clogRevIndex = fhc.changeset(fri);
-				// the way we built fileHistory ensures we won't walk past [changelogRevIndexStart..changelogRevIndexEnd]
-				assert clogRevIndex >= changelogRevIndexStart;
-				assert clogRevIndex <= changelogRevIndexEnd;
-				fhc.fillFileParents(fri, fileParentRevs);
-				fhc.fillCsetParents(fri, fileClogParentRevs);
-				bh.annotateChange(fri, clogRevIndex, fileParentRevs, fileClogParentRevs);
-			}
-		}
-	}
-
-	/**
-	 * Annotates changes of the file against its parent(s). 
-	 * Unlike {@link #annotate(HgDataFile, int, Inspector, HgIterateDirection)}, doesn't
-	 * walk file history, looks at the specified revision only. Handles both parents (if merge revision).
-	 */
-	public void annotateSingleRevision(int changelogRevisionIndex, Inspector insp) throws HgCallbackTargetException {
-		// TODO detect if file is text/binary (e.g. looking for chars < ' ' and not \t\r\n\f
-		int fileRevIndex = fileRevIndex(df, changelogRevisionIndex);
-		int[] fileRevParents = new int[2];
-		df.parents(fileRevIndex, fileRevParents, null, null);
-		if (changelogRevisionIndex == TIP) {
-			changelogRevisionIndex = df.getChangesetRevisionIndex(fileRevIndex);
-		}
-		BlameHelper bh = new BlameHelper(insp, 5);
-		bh.useFileUpTo(df, changelogRevisionIndex);
-		int[] fileClogParentRevs = new int[2];
-		fileClogParentRevs[0] = fileRevParents[0] == NO_REVISION ? NO_REVISION : df.getChangesetRevisionIndex(fileRevParents[0]);
-		fileClogParentRevs[1] = fileRevParents[1] == NO_REVISION ? NO_REVISION : df.getChangesetRevisionIndex(fileRevParents[1]);
-		bh.annotateChange(fileRevIndex, changelogRevisionIndex, fileRevParents, fileClogParentRevs);
-	}
-
-	/**
-	 * Client's sink for revision differences.
-	 * 
-	 * When implemented, clients shall not expect new {@link Block blocks} instances in each call.
-	 * 
-	 * In case more information about annotated revision is needed, inspector instances may supply 
-	 * {@link RevisionDescriptor.Recipient} through {@link Adaptable}.  
-	 */
-	@Callback
-	public interface Inspector {
-		void same(EqualBlock block) throws HgCallbackTargetException;
-		void added(AddBlock block) throws HgCallbackTargetException;
-		void changed(ChangeBlock block) throws HgCallbackTargetException;
-		void deleted(DeleteBlock block) throws HgCallbackTargetException;
-	}
-	
-	/**
-	 * Represents content of a block, either as a sequence of bytes or a 
-	 * sequence of smaller blocks (lines), if appropriate (according to usage context).
-	 * 
-	 * This approach allows line-by-line access to content data along with complete byte sequence for the whole block, i.e.
-	 * <pre>
-	 *    BlockData bd = addBlock.addedLines()
-	 *    // bd describes data from the addition completely.
-	 *    // elements of the BlockData are lines
-	 *    bd.elementCount() == addBlock.totalAddedLines();
-	 *    // one cat obtain complete addition with
-	 *    byte[] everythingAdded = bd.asArray();
-	 *    // or iterate line by line
-	 *    for (int i = 0; i < bd.elementCount(); i++) {
-	 *    	 byte[] lineContent = bd.elementAt(i);
-	 *       String line = new String(lineContent, fileEncodingCharset);
-	 *    }
-	 *    where bd.elementAt(0) is the line at index addBlock.firstAddedLine() 
-	 * </pre> 
-	 * 
-	 * LineData or ChunkData? 
-	 */
-	public interface BlockData {
-		BlockData elementAt(int index);
-		int elementCount();
-		byte[] asArray();
-	}
-	
-	/**
-	 * {@link Inspector} may optionally request extra information about revisions
-	 * being inspected, denoting itself as a {@link RevisionDescriptor.Recipient}. This class 
-	 * provides complete information about file revision under annotation now. 
-	 */
-	public interface RevisionDescriptor {
-		/**
-		 * @return complete source of the diff origin, never <code>null</code>
-		 */
-		BlockData origin();
-		/**
-		 * @return complete source of the diff target, never <code>null</code>
-		 */
-		BlockData target();
-		/**
-		 * @return changeset revision index of original file, or {@link HgRepository#NO_REVISION} if it's the very first revision
-		 */
-		int originChangesetIndex();
-		/**
-		 * @return changeset revision index of the target file
-		 */
-		int targetChangesetIndex();
-		/**
-		 * @return <code>true</code> if this revision is merge
-		 */
-		boolean isMerge();
-		/**
-		 * @return changeset revision index of the second, merged parent
-		 */
-		int mergeChangesetIndex();
-		/**
-		 * @return revision index of the change in target file's revlog
-		 */
-		int fileRevisionIndex();
-
-		/**
-		 * @return file object under blame (target file)
-		 */
-		HgDataFile file();
-
-		/**
-		 * Implement to indicate interest in {@link RevisionDescriptor}.
-		 * 
-		 * Note, instance of {@link RevisionDescriptor} is the same for 
-		 * {@link #start(RevisionDescriptor)} and {@link #done(RevisionDescriptor)} 
-		 * methods, and not necessarily a new one (i.e. <code>==</code>) for the next
-		 * revision announced.
-		 */
-		@Callback
-		public interface Recipient {
-			/**
-			 * Comes prior to any change {@link Block blocks}
-			 */
-			void start(RevisionDescriptor revisionDescription) throws HgCallbackTargetException;
-			/**
-			 * Comes after all change {@link Block blocks} were dispatched
-			 */
-			void done(RevisionDescriptor revisionDescription) throws HgCallbackTargetException;
-		}
-	}
-	
-	/**
-	 * Each change block comes from a single origin, blocks that are result of a merge
-	 * have {@link #originChangesetIndex()} equal to {@link RevisionDescriptor#mergeChangesetIndex()}.
-	 */
-	public interface Block {
-		int originChangesetIndex();
-		int targetChangesetIndex();
-	}
-	
-	public interface EqualBlock extends Block {
-		int originStart();
-		int targetStart();
-		int length();
-		BlockData content();
-	}
-	
-	public interface AddBlock extends Block {
-		/**
-		 * @return line index in the origin where this block is inserted
-		 */
-		int insertedAt();  
-		/**
-		 * @return line index of the first added line in the target revision
-		 */
-		int firstAddedLine();
-		/**
-		 * @return number of added lines in this block
-		 */
-		int totalAddedLines();
-		/**
-		 * @return content of added lines
-		 */
-		BlockData addedLines();
-	}
-	public interface DeleteBlock extends Block {
-		/**
-		 * @return line index in the target revision were this deleted block would be
-		 */
-		int removedAt();
-		/**
-		 * @return line index of the first removed line in the original revision
-		 */
-		int firstRemovedLine();
-		/**
-		 * @return number of deleted lines in this block
-		 */
-		int totalRemovedLines();
-		/**
-		 * @return content of deleted lines
-		 */
-		BlockData removedLines();
-	}
-	public interface ChangeBlock extends AddBlock, DeleteBlock {
-	}
-
-
-	private static int fileRevIndex(HgDataFile df, int csetRevIndex) {
-		Nodeid fileRev = df.getRepo().getManifest().getFileRevision(csetRevIndex, df.getPath());
-		return df.getRevisionIndex(fileRev);
-	}
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/repo/HgBlameInspector.java	Mon May 06 18:29:57 2013 +0200
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2013 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.repo;
+
+import org.tmatesoft.hg.core.HgCallbackTargetException;
+import org.tmatesoft.hg.internal.Callback;
+import org.tmatesoft.hg.internal.Experimental;
+import org.tmatesoft.hg.util.Adaptable;
+
+/**
+ * Client's sink for revision differences, diff/annotate functionality.
+ * 
+ * When implemented, clients shall not expect new {@link Block blocks} instances in each call.
+ * 
+ * In case more information about annotated revision is needed, inspector instances may supply 
+ * {@link RevisionDescriptor.Recipient} through {@link Adaptable}.  
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ * @since 1.1
+ */
+@Callback
+@Experimental(reason="Unstable API")
+public interface HgBlameInspector {
+
+	void same(EqualBlock block) throws HgCallbackTargetException;
+	void added(AddBlock block) throws HgCallbackTargetException;
+	void changed(ChangeBlock block) throws HgCallbackTargetException;
+	void deleted(DeleteBlock block) throws HgCallbackTargetException;
+	
+	/**
+	 * Represents content of a block, either as a sequence of bytes or a 
+	 * sequence of smaller blocks (lines), if appropriate (according to usage context).
+	 * 
+	 * This approach allows line-by-line access to content data along with complete byte sequence for the whole block, i.e.
+	 * <pre>
+	 *    BlockData bd = addBlock.addedLines()
+	 *    // bd describes data from the addition completely.
+	 *    // elements of the BlockData are lines
+	 *    bd.elementCount() == addBlock.totalAddedLines();
+	 *    // one cat obtain complete addition with
+	 *    byte[] everythingAdded = bd.asArray();
+	 *    // or iterate line by line
+	 *    for (int i = 0; i < bd.elementCount(); i++) {
+	 *    	 byte[] lineContent = bd.elementAt(i);
+	 *       String line = new String(lineContent, fileEncodingCharset);
+	 *    }
+	 *    where bd.elementAt(0) is the line at index addBlock.firstAddedLine() 
+	 * </pre> 
+	 * 
+	 * LineData or ChunkData? 
+	 */
+	public interface BlockData {
+		BlockData elementAt(int index);
+		int elementCount();
+		byte[] asArray();
+	}
+	
+	/**
+	 * {@link HgBlameInspector} may optionally request extra information about revisions
+	 * being inspected, denoting itself as a {@link RevisionDescriptor.Recipient}. This class 
+	 * provides complete information about file revision under annotation now. 
+	 */
+	public interface RevisionDescriptor {
+		/**
+		 * @return complete source of the diff origin, never <code>null</code>
+		 */
+		BlockData origin();
+		/**
+		 * @return complete source of the diff target, never <code>null</code>
+		 */
+		BlockData target();
+		/**
+		 * @return changeset revision index of original file, or {@link HgRepository#NO_REVISION} if it's the very first revision
+		 */
+		int originChangesetIndex();
+		/**
+		 * @return changeset revision index of the target file
+		 */
+		int targetChangesetIndex();
+		/**
+		 * @return <code>true</code> if this revision is merge
+		 */
+		boolean isMerge();
+		/**
+		 * @return changeset revision index of the second, merged parent
+		 */
+		int mergeChangesetIndex();
+		/**
+		 * @return revision index of the change in target file's revlog
+		 */
+		int fileRevisionIndex();
+
+		/**
+		 * @return file object under blame (target file)
+		 */
+		HgDataFile file();
+
+		/**
+		 * Implement to indicate interest in {@link RevisionDescriptor}.
+		 * 
+		 * Note, instance of {@link RevisionDescriptor} is the same for 
+		 * {@link #start(RevisionDescriptor)} and {@link #done(RevisionDescriptor)} 
+		 * methods, and not necessarily a new one (i.e. <code>==</code>) for the next
+		 * revision announced.
+		 */
+		@Callback
+		public interface Recipient {
+			/**
+			 * Comes prior to any change {@link Block blocks}
+			 */
+			void start(RevisionDescriptor revisionDescription) throws HgCallbackTargetException;
+			/**
+			 * Comes after all change {@link Block blocks} were dispatched
+			 */
+			void done(RevisionDescriptor revisionDescription) throws HgCallbackTargetException;
+		}
+	}
+	
+	/**
+	 * Each change block comes from a single origin, blocks that are result of a merge
+	 * have {@link #originChangesetIndex()} equal to {@link RevisionDescriptor#mergeChangesetIndex()}.
+	 */
+	public interface Block {
+		int originChangesetIndex();
+		int targetChangesetIndex();
+	}
+	
+	public interface EqualBlock extends Block {
+		int originStart();
+		int targetStart();
+		int length();
+		BlockData content();
+	}
+	
+	public interface AddBlock extends Block {
+		/**
+		 * @return line index in the origin where this block is inserted
+		 */
+		int insertedAt();  
+		/**
+		 * @return line index of the first added line in the target revision
+		 */
+		int firstAddedLine();
+		/**
+		 * @return number of added lines in this block
+		 */
+		int totalAddedLines();
+		/**
+		 * @return content of added lines
+		 */
+		BlockData addedLines();
+	}
+	public interface DeleteBlock extends Block {
+		/**
+		 * @return line index in the target revision were this deleted block would be
+		 */
+		int removedAt();
+		/**
+		 * @return line index of the first removed line in the original revision
+		 */
+		int firstRemovedLine();
+		/**
+		 * @return number of deleted lines in this block
+		 */
+		int totalRemovedLines();
+		/**
+		 * @return content of deleted lines
+		 */
+		BlockData removedLines();
+	}
+	public interface ChangeBlock extends AddBlock, DeleteBlock {
+	}
+}
--- a/src/org/tmatesoft/hg/repo/HgDataFile.java	Mon May 06 17:11:29 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgDataFile.java	Mon May 06 18:29:57 2013 +0200
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2012 TMate Software Ltd
+ * Copyright (c) 2010-2013 TMate Software Ltd
  *  
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,6 +16,7 @@
  */
 package org.tmatesoft.hg.repo;
 
+import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew;
 import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex;
 import static org.tmatesoft.hg.repo.HgRepository.*;
 import static org.tmatesoft.hg.util.LogFacility.Severity.Info;
@@ -28,14 +29,20 @@
 import java.nio.channels.FileChannel;
 import java.util.Arrays;
 
+import org.tmatesoft.hg.core.HgCallbackTargetException;
 import org.tmatesoft.hg.core.HgChangesetFileSneaker;
+import org.tmatesoft.hg.core.HgIterateDirection;
 import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.internal.BlameHelper;
 import org.tmatesoft.hg.internal.DataAccess;
+import org.tmatesoft.hg.internal.FileHistory;
+import org.tmatesoft.hg.internal.FileRevisionHistoryChunk;
 import org.tmatesoft.hg.internal.FilterByteChannel;
 import org.tmatesoft.hg.internal.FilterDataAccess;
 import org.tmatesoft.hg.internal.Internals;
 import org.tmatesoft.hg.internal.Metadata;
 import org.tmatesoft.hg.internal.RevlogStream;
+import org.tmatesoft.hg.repo.HgBlameInspector;
 import org.tmatesoft.hg.util.ByteChannel;
 import org.tmatesoft.hg.util.CancelSupport;
 import org.tmatesoft.hg.util.CancelledException;
@@ -428,6 +435,86 @@
 		return getRepo().getManifest().getFileFlags(changesetRevIndex, getPath());
 	}
 	
+	/**
+	 * mimic 'hg diff -r clogRevIndex1 -r clogRevIndex2'
+	 */
+	public void diff(int clogRevIndex1, int clogRevIndex2, HgBlameInspector insp) throws HgCallbackTargetException {
+		// FIXME clogRevIndex1 and clogRevIndex2 may point to different files, need to decide whether to throw an exception
+		// or to attempt to look up correct file node (tricky)
+		int fileRevIndex1 = fileRevIndex(this, clogRevIndex1);
+		int fileRevIndex2 = fileRevIndex(this, clogRevIndex2);
+		BlameHelper bh = new BlameHelper(insp, 5);
+		bh.useFileUpTo(this, clogRevIndex2);
+		bh.diff(fileRevIndex1, clogRevIndex1, fileRevIndex2, clogRevIndex2);
+	}
+	
+	/**
+	 * Walk file history up/down to revision at given changeset and report changes for each revision
+	 */
+	public void annotate(int changelogRevisionIndex, HgBlameInspector insp, HgIterateDirection iterateOrder) throws HgCallbackTargetException {
+		annotate(0, changelogRevisionIndex, insp, iterateOrder);
+	}
+
+	/**
+	 * Walk file history range and report changes for each revision
+	 */
+	public void annotate(int changelogRevIndexStart, int changelogRevIndexEnd, HgBlameInspector insp, HgIterateDirection iterateOrder) throws HgCallbackTargetException {
+		if (wrongRevisionIndex(changelogRevIndexStart) || wrongRevisionIndex(changelogRevIndexEnd)) {
+			throw new IllegalArgumentException();
+		}
+		// Note, changelogRevIndexEnd may be TIP, while the code below doesn't tolerate constants
+		//
+		int lastRevision = getRepo().getChangelog().getLastRevision();
+		if (changelogRevIndexEnd == TIP) {
+			changelogRevIndexEnd = lastRevision;
+		}
+		HgInternals.checkRevlogRange(changelogRevIndexStart, changelogRevIndexEnd, lastRevision);
+		if (!exists()) {
+			return;
+		}
+		FileHistory fileHistory = new FileHistory(this, changelogRevIndexStart, changelogRevIndexEnd);
+		fileHistory.build();
+		BlameHelper bh = new BlameHelper(insp, 10);
+		for (FileRevisionHistoryChunk fhc : fileHistory.iterate(OldToNew)) {
+			// iteration order is not important here
+			bh.useFileUpTo(fhc.getFile(), fhc.getEndChangeset());
+		}
+		int[] fileClogParentRevs = new int[2];
+		int[] fileParentRevs = new int[2];
+		for (FileRevisionHistoryChunk fhc : fileHistory.iterate(iterateOrder)) {
+			for (int fri : fhc.fileRevisions(iterateOrder)) {
+				int clogRevIndex = fhc.changeset(fri);
+				// the way we built fileHistory ensures we won't walk past [changelogRevIndexStart..changelogRevIndexEnd]
+				assert clogRevIndex >= changelogRevIndexStart;
+				assert clogRevIndex <= changelogRevIndexEnd;
+				fhc.fillFileParents(fri, fileParentRevs);
+				fhc.fillCsetParents(fri, fileClogParentRevs);
+				bh.annotateChange(fri, clogRevIndex, fileParentRevs, fileClogParentRevs);
+			}
+		}
+	}
+
+	/**
+	 * Annotates changes of the file against its parent(s). 
+	 * Unlike {@link #annotate(HgDataFile, int, Inspector, HgIterateDirection)}, doesn't
+	 * walk file history, looks at the specified revision only. Handles both parents (if merge revision).
+	 */
+	public void annotateSingleRevision(int changelogRevisionIndex, HgBlameInspector insp) throws HgCallbackTargetException {
+		// TODO detect if file is text/binary (e.g. looking for chars < ' ' and not \t\r\n\f
+		int fileRevIndex = fileRevIndex(this, changelogRevisionIndex);
+		int[] fileRevParents = new int[2];
+		parents(fileRevIndex, fileRevParents, null, null);
+		if (changelogRevisionIndex == TIP) {
+			changelogRevisionIndex = getChangesetRevisionIndex(fileRevIndex);
+		}
+		BlameHelper bh = new BlameHelper(insp, 5);
+		bh.useFileUpTo(this, changelogRevisionIndex);
+		int[] fileClogParentRevs = new int[2];
+		fileClogParentRevs[0] = fileRevParents[0] == NO_REVISION ? NO_REVISION : getChangesetRevisionIndex(fileRevParents[0]);
+		fileClogParentRevs[1] = fileRevParents[1] == NO_REVISION ? NO_REVISION : getChangesetRevisionIndex(fileRevParents[1]);
+		bh.annotateChange(fileRevIndex, changelogRevisionIndex, fileRevParents, fileClogParentRevs);
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder sb = new StringBuilder(getClass().getSimpleName());
@@ -445,6 +532,13 @@
 		RevlogStream.Inspector insp = new MetadataInspector(metadata, null);
 		super.content.iterate(localRev, localRev, true, insp);
 	}
+	
+
+	private static int fileRevIndex(HgDataFile df, int csetRevIndex) {
+		Nodeid fileRev = df.getRepo().getManifest().getFileRevision(csetRevIndex, df.getPath());
+		return df.getRevisionIndex(fileRev);
+	}
+
 
 	private static class MetadataInspector extends ErrorHandlingInspector implements RevlogStream.Inspector {
 		private final Metadata metadata;
--- a/test/org/tmatesoft/hg/test/TestBlame.java	Mon May 06 17:11:29 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestBlame.java	Mon May 06 18:29:57 2013 +0200
@@ -49,13 +49,8 @@
 import org.tmatesoft.hg.internal.FileAnnotation.LineDescriptor;
 import org.tmatesoft.hg.internal.FileAnnotation.LineInspector;
 import org.tmatesoft.hg.internal.IntVector;
-import org.tmatesoft.hg.repo.HgBlameFacility;
-import org.tmatesoft.hg.repo.HgBlameFacility.AddBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.Block;
-import org.tmatesoft.hg.repo.HgBlameFacility.BlockData;
-import org.tmatesoft.hg.repo.HgBlameFacility.ChangeBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.DeleteBlock;
-import org.tmatesoft.hg.repo.HgBlameFacility.EqualBlock;
+import org.tmatesoft.hg.repo.HgBlameInspector;
+import org.tmatesoft.hg.repo.HgBlameInspector.BlockData;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgLookup;
 import org.tmatesoft.hg.repo.HgRepository;
@@ -79,7 +74,7 @@
 		final int checkChangeset = 539;
 		HgDataFile df = repo.getFileNode(fname);
 		ByteArrayOutputStream bos = new ByteArrayOutputStream();
-		new HgBlameFacility(df).annotateSingleRevision(checkChangeset, new DiffOutInspector(new PrintStream(bos)));
+		df.annotateSingleRevision(checkChangeset, new DiffOutInspector(new PrintStream(bos)));
 		LineGrepOutputParser gp = new LineGrepOutputParser("^@@.+");
 		ExecHelper eh = new ExecHelper(gp, null);
 		eh.run("hg", "diff", "-c", String.valueOf(checkChangeset), "-U", "0", fname);
@@ -135,10 +130,9 @@
 	public void testComplexHistoryAnnotate() throws Exception {
 		HgRepository repo = Configuration.get().find("test-annotate");
 		HgDataFile df = repo.getFileNode("file1");
-		HgBlameFacility af = new HgBlameFacility(df);
 		ByteArrayOutputStream bos = new ByteArrayOutputStream();
 		DiffOutInspector dump = new DiffOutInspector(new PrintStream(bos));
-		af.annotate(TIP, dump, HgIterateDirection.OldToNew);
+		df.annotate(TIP, dump, HgIterateDirection.OldToNew);
 		LinkedList<String> apiResult = new LinkedList<String>(Arrays.asList(splitLines(bos.toString())));
 		
 		/*
@@ -196,7 +190,6 @@
 		HgRepository repo = Configuration.get().find("test-annotate2");
 		HgDataFile df = repo.getFileNode("file1b.txt");
 		// rev3: file1 -> file1a,  rev7: file1a -> file1b, tip: rev10
-		HgBlameFacility bf = new HgBlameFacility(df);
 		DiffOutInspector insp = new DiffOutInspector(new PrintStream(new OutputStream() {
 			@Override
 			public void write(int b) throws IOException {
@@ -207,19 +200,19 @@
 		// earlier than rev2 shall be reported as new from change3
 		int[] change_2_8_new2old = new int[] {4, 6, 3, 4, -1, 3}; 
 		int[] change_2_8_old2new = new int[] {-1, 3, 3, 4, 4, 6 };
-		bf.annotate(2, 8, insp, NewToOld);
+		df.annotate(2, 8, insp, NewToOld);
 		Assert.assertArrayEquals(change_2_8_new2old, insp.getReportedRevisionPairs());
 		insp.reset();
-		bf.annotate(2, 8, insp, OldToNew);
+		df.annotate(2, 8, insp, OldToNew);
 		Assert.assertArrayEquals(change_2_8_old2new, insp.getReportedRevisionPairs());
 		// same as 2 to 8, with addition of rev9 changes rev7  (rev6 to rev7 didn't change content, only name)
 		int[] change_3_9_new2old = new int[] {7, 9, 4, 6, 3, 4, -1, 3 }; 
 		int[] change_3_9_old2new = new int[] {-1, 3, 3, 4, 4, 6, 7, 9 };
 		insp.reset();
-		bf.annotate(3, 9, insp, NewToOld);
+		df.annotate(3, 9, insp, NewToOld);
 		Assert.assertArrayEquals(change_3_9_new2old, insp.getReportedRevisionPairs());
 		insp.reset();
-		bf.annotate(3, 9, insp, OldToNew);
+		df.annotate(3, 9, insp, OldToNew);
 		Assert.assertArrayEquals(change_3_9_old2new, insp.getReportedRevisionPairs());
 	}
 
@@ -297,18 +290,17 @@
 		final String fname = "src/org/tmatesoft/hg/internal/PatchGenerator.java";
 		final int checkChangeset = 539;
 		HgDataFile df = repo.getFileNode(fname);
-		HgBlameFacility af = new HgBlameFacility(df);
 		DiffOutInspector dump = new DiffOutInspector(System.out);
 		System.out.println("541 -> 543");
-		af.annotateSingleRevision(543, dump);
+		df.annotateSingleRevision(543, dump);
 		System.out.println("539 -> 541");
-		af.annotateSingleRevision(541, dump);
+		df.annotateSingleRevision(541, dump);
 		System.out.println("536 -> 539");
-		af.annotateSingleRevision(checkChangeset, dump);
+		df.annotateSingleRevision(checkChangeset, dump);
 		System.out.println("531 -> 536");
-		af.annotateSingleRevision(536, dump);
+		df.annotateSingleRevision(536, dump);
 		System.out.println(" -1 -> 531");
-		af.annotateSingleRevision(531, dump);
+		df.annotateSingleRevision(531, dump);
 		
 		FileAnnotateInspector fai = new FileAnnotateInspector();
 		FileAnnotation.annotate(df, 541, fai);
@@ -322,7 +314,6 @@
 		final String fname = "src/org/tmatesoft/hg/repo/HgManifest.java";
 		final int checkChangeset = 415;
 		HgDataFile df = repo.getFileNode(fname);
-		HgBlameFacility af = new HgBlameFacility(df);
 		DiffOutInspector dump = new DiffOutInspector(System.out);
 //		System.out.println("413 -> 415");
 //		af.diff(df, 413, 415, dump);
@@ -332,16 +323,15 @@
 //		dump.needRevisions(true);
 //		af.annotateChange(df, checkChangeset, dump);
 		dump.needRevisions(true);
-		af.annotate(checkChangeset, dump, HgIterateDirection.OldToNew);
+		df.annotate(checkChangeset, dump, HgIterateDirection.OldToNew);
 	}
 	
 	private void ccc() throws Throwable {
 		HgRepository repo = new HgLookup().detect("/home/artem/hg/hgtest-annotate-merge/");
 		HgDataFile df = repo.getFileNode("file.txt");
-		HgBlameFacility af = new HgBlameFacility(df);
 		DiffOutInspector dump = new DiffOutInspector(System.out);
 		dump.needRevisions(true);
-		af.annotate(8, dump, HgIterateDirection.NewToOld);
+		df.annotate(8, dump, HgIterateDirection.NewToOld);
 //		af.annotateSingleRevision(df, 113, dump);
 //		System.out.println();
 //		af.annotate(df, TIP, new LineDumpInspector(true), HgIterateDirection.NewToOld);
@@ -372,7 +362,7 @@
 		tt.ccc();
 	}
 
-	private static class DiffOutInspector implements HgBlameFacility.Inspector {
+	private static class DiffOutInspector implements HgBlameInspector {
 		private final PrintStream out;
 		private boolean dumpRevs;
 		private IntVector reportedRevisionPairs = new IntVector();
@@ -490,7 +480,8 @@
 		}
 	}
 
-	private static class LineDumpInspector implements HgBlameFacility.Inspector {
+	@SuppressWarnings("unused")
+	private static class LineDumpInspector implements HgBlameInspector {
 		
 		private final boolean lineByLine;