changeset 676:3219cfadda49

Switch to alternative annotate producer (walks from parents to children). Refactor FileAnnotation to match updated annotate approach
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 18 Jul 2013 18:03:51 +0200
parents a20121a2bba6
children 1c49c0cee540
files src/org/tmatesoft/hg/core/HgAnnotateCommand.java src/org/tmatesoft/hg/internal/FileAnnotation.java src/org/tmatesoft/hg/internal/ForwardAnnotateInspector.java src/org/tmatesoft/hg/internal/LineImpl.java src/org/tmatesoft/hg/internal/ReverseAnnotateInspector.java test/org/tmatesoft/hg/test/TestBlame.java
diffstat 6 files changed, 422 insertions(+), 280 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgAnnotateCommand.java	Thu Jul 18 18:02:36 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgAnnotateCommand.java	Thu Jul 18 18:03:51 2013 +0200
@@ -16,16 +16,9 @@
  */
 package org.tmatesoft.hg.core;
 
-import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
-
-import java.util.Arrays;
-
-import org.tmatesoft.hg.core.HgBlameInspector.BlockData;
 import org.tmatesoft.hg.internal.Callback;
 import org.tmatesoft.hg.internal.CsetParamKeeper;
-import org.tmatesoft.hg.internal.FileAnnotation;
-import org.tmatesoft.hg.internal.FileAnnotation.LineDescriptor;
-import org.tmatesoft.hg.internal.FileAnnotation.LineInspector;
+import org.tmatesoft.hg.internal.ForwardAnnotateInspector;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgRuntimeException;
@@ -88,6 +81,7 @@
 	}
 	
 	// TODO [post-1.1] set encoding and provide String line content from LineInfo
+	// TODO FWIW: diff algorithms: http://bramcohen.livejournal.com/73318.html
 
 	/**
 	 * Annotate selected file
@@ -107,32 +101,26 @@
 		final ProgressSupport progress = getProgressSupport(inspector);
 		final CancelSupport cancellation = getCancelSupport(inspector, true);
 		cancellation.checkCancelled();
-		progress.start(2);
+		progress.start(200);
 		try {
 			HgDataFile df = repo.getFileNode(file);
 			if (!df.exists()) {
 				return;
 			}
 			final int changesetStart = followRename ? 0 : df.getChangesetRevisionIndex(0);
-			Collector c = new Collector(cancellation);
-			FileAnnotation fa = new FileAnnotation(c);
-			HgDiffCommand cmd = new HgDiffCommand(repo);
-			cmd.file(df).order(HgIterateDirection.NewToOld);
-			cmd.range(changesetStart, annotateRevision.get());
-			cmd.executeAnnotate(fa);
-			progress.worked(1);
-			c.throwIfCancelled();
+			final int annotateRevIndex = annotateRevision.get();
+			HgDiffCommand cmd = new HgDiffCommand(repo).file(df);
+			cmd.range(changesetStart, annotateRevIndex);
+			cmd.set(cancellation);
+			cmd.set(new ProgressSupport.Sub(progress, 100));
+			//
+//			ReverseAnnotateInspector ai = new ReverseAnnotateInspector();
+			ForwardAnnotateInspector ai = new ForwardAnnotateInspector();
+			cmd.order(ai.iterateDirection());
+			//
+			cmd.executeAnnotate(ai);
 			cancellation.checkCancelled();
-			ProgressSupport.Sub subProgress = new ProgressSupport.Sub(progress, 1);
-			subProgress.start(c.lineRevisions.length);
-			LineImpl li = new LineImpl();
-			for (int i = 0; i < c.lineRevisions.length; i++) {
-				li.init(i+1, c.lineRevisions[i], c.line(i));
-				inspector.next(li);
-				subProgress.worked(1);
-				cancellation.checkCancelled();
-			}
-			subProgress.done();
+			ai.report(annotateRevIndex, inspector, new ProgressSupport.Sub(progress, 100), cancellation);
 		} catch (HgRuntimeException ex) {
 			throw new HgLibraryFailureException(ex);
 		}
@@ -159,70 +147,4 @@
 		int getChangesetIndex();
 		byte[] getContent();
 	}
-
-	// TODO [post-1.1] there's no need in FileAnnotation.LineInspector, merge it here
-	// ok for 1.1 as this LineInspector is internal class
-	private static class Collector implements LineInspector {
-		private int[] lineRevisions;
-		private byte[][] lines;
-		private final CancelSupport cancelSupport;
-		private CancelledException cancelEx;
-		
-		Collector(CancelSupport cancellation) {
-			cancelSupport = cancellation;
-		}
-		
-		public void line(int lineNumber, int changesetRevIndex, BlockData lineContent, LineDescriptor ld) {
-			if (cancelEx != null) {
-				return;
-			}
-			if (lineRevisions == null) {
-				lineRevisions = new int [ld.totalLines()];
-				Arrays.fill(lineRevisions, NO_REVISION);
-				lines = new byte[ld.totalLines()][];
-			}
-			lineRevisions[lineNumber] = changesetRevIndex;
-			lines[lineNumber] = lineContent.asArray();
-			try {
-				cancelSupport.checkCancelled();
-			} catch (CancelledException ex) {
-				cancelEx = ex;
-			}
-		}
-		
-		public byte[] line(int i) {
-			return lines[i];
-		}
-		
-		public void throwIfCancelled() throws CancelledException {
-			if (cancelEx != null) {
-				throw cancelEx;
-			}
-		}
-	}
-	
-	
-	private static class LineImpl implements LineInfo {
-		private int ln;
-		private int rev;
-		private byte[] content;
-
-		void init(int line, int csetRev, byte[] cnt) {
-			ln = line;
-			rev = csetRev;
-			content = cnt;
-		}
-
-		public int getLineNumber() {
-			return ln;
-		}
-
-		public int getChangesetIndex() {
-			return rev;
-		}
-
-		public byte[] getContent() {
-			return content;
-		}
-	}
 }
--- a/src/org/tmatesoft/hg/internal/FileAnnotation.java	Thu Jul 18 18:02:36 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +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.internal;
-
-
-import org.tmatesoft.hg.core.HgBlameInspector;
-import org.tmatesoft.hg.core.HgBlameInspector.RevisionDescriptor;
-import org.tmatesoft.hg.repo.HgInvalidStateException;
-
-/**
- * Produce output like 'hg annotate' does
- * 
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-public class FileAnnotation implements HgBlameInspector, RevisionDescriptor.Recipient {
-
-	@Experimental(reason="The line-by-line inspector likely to become part of core/command API")
-	@Callback
-	public interface LineInspector {
-		/**
-		 * Not necessarily invoked sequentially by line numbers
-		 */
-		void line(int lineNumber, int changesetRevIndex, BlockData lineContent, LineDescriptor ld);
-	}
-
-	public interface LineDescriptor {
-		int totalLines();
-	}
-
-	// keeps <startSeq1, startSeq2, len> of equal blocks, origin to target, from some previous step
-	private RangePairSeq activeEquals;
-	// equal blocks of the current iteration, to be recalculated before next step
-	// to track line number (current target to ultimate target) mapping 
-	private RangePairSeq intermediateEquals = new RangePairSeq();
-
-	private boolean[] knownLines;
-	private final LineInspector delegate;
-	private RevisionDescriptor revisionDescriptor;
-	private BlockData lineContent;
-
-	private IntMap<RangePairSeq> mergedRanges = new IntMap<RangePairSeq>(10);
-	private IntMap<RangePairSeq> equalRanges = new IntMap<RangePairSeq>(10);
-	private boolean activeEqualsComesFromMerge = false;
-
-	public FileAnnotation(LineInspector lineInspector) {
-		delegate = lineInspector;
-	}
-
-	public void start(RevisionDescriptor rd) {
-		revisionDescriptor = rd;
-		if (knownLines == null) {
-			lineContent = rd.target();
-			knownLines = new boolean[lineContent.elementCount()];
-			activeEquals = new RangePairSeq();
-			activeEquals.add(0, 0, knownLines.length);
-			equalRanges.put(rd.targetChangesetIndex(), activeEquals);
-		} else {
-			activeEquals = equalRanges.get(rd.targetChangesetIndex());
-			if (activeEquals == null) {
-				// we didn't see this target revision as origin yet
-				// the only way this may happen is that this revision was a merge parent
-				activeEquals = mergedRanges.get(rd.targetChangesetIndex());
-				activeEqualsComesFromMerge = true;
-				if (activeEquals == null) {
-					throw new HgInvalidStateException(String.format("Can't find previously visited revision %d (while in %d->%1$d diff)", rd.targetChangesetIndex(), rd.originChangesetIndex()));
-				}
-			}
-		}
-	}
-
-	public void done(RevisionDescriptor rd) {
-		// update line numbers of the intermediate target to point to ultimate target's line numbers
-		RangePairSeq v = intermediateEquals.intersect(activeEquals);
-		if (activeEqualsComesFromMerge) {
-			mergedRanges.put(rd.originChangesetIndex(), v);
-		} else {
-			equalRanges.put(rd.originChangesetIndex(), v);
-		}
-		if (rd.isMerge() && !mergedRanges.containsKey(rd.mergeChangesetIndex())) {
-			// seen merge, but no lines were merged from p2.
-			// Add empty range to avoid uncertainty when a parent of p2 pops in
-			mergedRanges.put(rd.mergeChangesetIndex(), new RangePairSeq());
-		}
-		intermediateEquals.clear();
-		activeEquals = null;
-		activeEqualsComesFromMerge = false;
-		revisionDescriptor = null;
-	}
-
-	public void same(EqualBlock block) {
-		intermediateEquals.add(block.originStart(), block.targetStart(), block.length());
-	}
-
-	public void added(AddBlock block) {
-		RangePairSeq rs = null;
-		if (revisionDescriptor.isMerge() && block.originChangesetIndex() == revisionDescriptor.mergeChangesetIndex()) {
-			rs = mergedRanges.get(revisionDescriptor.mergeChangesetIndex());
-			if (rs == null) {
-				mergedRanges.put(revisionDescriptor.mergeChangesetIndex(), rs = new RangePairSeq());
-			}
-		}
-		if (activeEquals.size() == 0) {
-			return;
-		}
-		for (int i = 0, ln = block.firstAddedLine(), x = block.totalAddedLines(); i < x; i++, ln++) {
-			int lnInFinal = activeEquals.mapLineIndex(ln);
-			if (lnInFinal != -1/* && !knownLines[lnInFinal]*/) {
-				if (rs != null) {
-					rs.add(block.insertedAt() + i, lnInFinal, 1);
-				} else {
-					delegate.line(lnInFinal, block.targetChangesetIndex(), lineContent.elementAt(lnInFinal), new LineDescriptorImpl());
-				}
-				knownLines[lnInFinal] = true;
-			}
-		}
-	}
-
-	public void changed(ChangeBlock block) {
-		added(block);
-	}
-
-	public void deleted(DeleteBlock block) {
-	}
-
-	private final class LineDescriptorImpl implements LineDescriptor {
-		LineDescriptorImpl() {
-		}
-
-		public int totalLines() {
-			return FileAnnotation.this.knownLines.length;
-		}
-	}
-}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/ForwardAnnotateInspector.java	Thu Jul 18 18:03:51 2013 +0200
@@ -0,0 +1,163 @@
+/*
+ * 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.internal;
+
+import org.tmatesoft.hg.core.HgAnnotateCommand.Inspector;
+import org.tmatesoft.hg.core.HgAnnotateCommand.LineInfo;
+import org.tmatesoft.hg.core.HgBlameInspector;
+import org.tmatesoft.hg.core.HgCallbackTargetException;
+import org.tmatesoft.hg.core.HgDiffCommand;
+import org.tmatesoft.hg.core.HgException;
+import org.tmatesoft.hg.core.HgIterateDirection;
+import org.tmatesoft.hg.repo.HgLookup;
+import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.util.CancelSupport;
+import org.tmatesoft.hg.util.CancelledException;
+import org.tmatesoft.hg.util.ProgressSupport;
+
+/**
+ * Annotate file history iterating from parents to children
+ * 
+ * At the moment, doesn't handle start from any revision but 0
+ * 
+ * (+) May report annotate for any revision in the visited range.
+ * 
+ * @see ReverseAnnotateInspector
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class ForwardAnnotateInspector implements HgBlameInspector, HgBlameInspector.RevisionDescriptor.Recipient {
+	final IntMap<IntSliceSeq> all = new IntMap<IntSliceSeq>(100);
+	// revision->map(lineNumber->lineContent)
+	private final IntMap<IntMap<byte[]>> lineContent = new IntMap<IntMap<byte[]>>(100);
+	private IntSliceSeq current;
+	private RevisionDescriptor revDescriptor;
+
+	/**
+	 * @return desired order of iteration for diff
+	 */
+	public HgIterateDirection iterateDirection() {
+		return HgIterateDirection.OldToNew;
+	}
+
+	public void report(int revision, Inspector insp, ProgressSupport progress, CancelSupport cancel) throws HgCallbackTargetException, CancelledException {
+		int totalLines = 0;
+		for (IntTuple t : all.get(revision)) {
+			totalLines += t.at(0);
+		}
+		progress.start(totalLines);
+		LineImpl li = new LineImpl();
+		int line = 1;
+		for (IntTuple t : all.get(revision)) {
+			IntMap<byte[]> revLines = lineContent.get(t.at(1));
+			for (int i = 0, x = t.at(0); i < x; i++) {
+				final int lineInRev = t.at(2) + i;
+				final byte[] lc = revLines.get(lineInRev);
+				li.init(line++, t.at(1), lc);
+				insp.next(li);
+				progress.worked(1);
+				cancel.checkCancelled();
+			}
+		}
+		progress.done();
+	}
+
+	public void start(RevisionDescriptor rd) throws HgCallbackTargetException {
+		all.put(rd.targetChangesetIndex(), current = new IntSliceSeq(3));
+		revDescriptor = rd;
+	}
+
+	public void done(RevisionDescriptor rd) throws HgCallbackTargetException {
+		revDescriptor = null;
+	}
+
+	public void same(EqualBlock block) throws HgCallbackTargetException {
+		copyBlock(block.originChangesetIndex(), block.originStart(), block.length());
+	}
+
+	public void added(AddBlock block) throws HgCallbackTargetException {
+		if (revDescriptor.isMerge() && block.originChangesetIndex() == revDescriptor.mergeChangesetIndex()) {
+			copyBlock(block.originChangesetIndex(), block.insertedAt(), block.totalAddedLines());
+			return;
+		}
+		BlockData addedLines = block.addedLines();
+		IntMap<byte[]> revLines = lineContent.get(block.targetChangesetIndex());
+		if (revLines == null) {
+			lineContent.put(block.targetChangesetIndex(), revLines = new IntMap<byte[]>(block.totalAddedLines()));
+		}
+		for (int i = 0; i < block.totalAddedLines(); i++) {
+			revLines.put(block.firstAddedLine() + i, addedLines.elementAt(i).asArray());
+		}
+		current.add(block.totalAddedLines(), block.targetChangesetIndex(), block.firstAddedLine());
+	}
+
+	public void changed(ChangeBlock block) throws HgCallbackTargetException {
+		added(block);
+	}
+
+	public void deleted(DeleteBlock block) throws HgCallbackTargetException {
+	}
+	
+	private void copyBlock(int originChangesetIndex, int originStart, int length) {
+		IntSliceSeq origin = all.get(originChangesetIndex);
+		assert origin != null; // shall visit parents before came to this child
+		int originPos = 0;
+		int targetBlockLen = length;
+		for (IntTuple t : origin) {
+			int originBlockLen = t.at(0);
+			int originBlockEnd = originPos + originBlockLen;
+			if (originBlockEnd > originStart) {
+				int originBlockOverlap = Math.min(originBlockLen, originBlockEnd - originStart);
+				assert originBlockOverlap > 0;
+				originBlockOverlap = Math.min(originBlockOverlap, targetBlockLen);
+				int originBlockLine = t.at(2);
+				if (originPos < originStart) {
+					originBlockLine += originBlockLen-originBlockOverlap;
+				}
+				// copy fragment of original block;
+				current.add(originBlockOverlap, t.at(1), originBlockLine);
+				targetBlockLen -= originBlockOverlap;
+				if (targetBlockLen == 0) {
+					break;
+				}
+			}
+			originPos += originBlockLen;
+		}
+	}
+
+
+	public static void main(String[] args) throws HgCallbackTargetException, CancelledException, HgException {
+		HgRepository repo = new HgLookup().detect("/home/artem/hg/junit-test-repos/test-annotate/");
+		HgDiffCommand cmd = new HgDiffCommand(repo);
+		cmd.file(repo.getFileNode("file1")).order(HgIterateDirection.OldToNew);
+		cmd.range(0, 8);
+		final ForwardAnnotateInspector c2 = new ForwardAnnotateInspector();
+		cmd.executeAnnotate(c2);
+		for (IntTuple t : c2.all.get(8)) {
+			System.out.printf("Block %d lines from revision %d (starts with line %d in the origin)\n", t.at(0), t.at(1), t.at(2));
+		}
+		for (IntTuple t : c2.all.get(8)) {
+			System.out.printf("Block %d lines from revision %d (starts with line %d in the origin)\n", t.at(0), t.at(1), 1+t.at(2));
+		}
+		c2.report(8, new Inspector() {
+			
+			public void next(LineInfo lineInfo) throws HgCallbackTargetException {
+				System.out.printf("%3d:%3d: %s", lineInfo.getChangesetIndex(), lineInfo.getLineNumber(), new String(lineInfo.getContent()));
+			}
+		}, ProgressSupport.Factory.get(null), CancelSupport.Factory.get(null));
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/LineImpl.java	Thu Jul 18 18:03:51 2013 +0200
@@ -0,0 +1,47 @@
+/*
+ * 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.internal;
+
+import org.tmatesoft.hg.core.HgAnnotateCommand.LineInfo;
+
+/**
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+final class LineImpl implements LineInfo {
+	private int ln;
+	private int rev;
+	private byte[] content;
+
+	void init(int line, int csetRev, byte[] cnt) {
+		ln = line;
+		rev = csetRev;
+		content = cnt;
+	}
+
+	public int getLineNumber() {
+		return ln;
+	}
+
+	public int getChangesetIndex() {
+		return rev;
+	}
+
+	public byte[] getContent() {
+		return content;
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/ReverseAnnotateInspector.java	Thu Jul 18 18:03:51 2013 +0200
@@ -0,0 +1,164 @@
+/*
+ * 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.internal;
+
+
+import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
+
+import java.util.Arrays;
+
+import org.tmatesoft.hg.core.HgAnnotateCommand;
+import org.tmatesoft.hg.core.HgBlameInspector;
+import org.tmatesoft.hg.core.HgIterateDirection;
+import org.tmatesoft.hg.core.HgBlameInspector.RevisionDescriptor;
+import org.tmatesoft.hg.core.HgCallbackTargetException;
+import org.tmatesoft.hg.repo.HgInvalidStateException;
+import org.tmatesoft.hg.util.CancelSupport;
+import org.tmatesoft.hg.util.CancelledException;
+import org.tmatesoft.hg.util.ProgressSupport;
+
+/**
+ * Produce output like 'hg annotate' does.
+ * Expects revisions to come in order from child to parent.
+ * Unlike {@link ForwardAnnotateInspector}, can be easily modified to report lines as soon as its origin is detected.
+ * 
+ * (+) Handles annotate of partial history, at any moment lines with ({@link #knownLines} == <code>false</code> indicate lines
+ * that were added prior to any revision already visited. 
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class ReverseAnnotateInspector implements HgBlameInspector, RevisionDescriptor.Recipient {
+
+	// keeps <startSeq1, startSeq2, len> of equal blocks, origin to target, from some previous step
+	private RangePairSeq activeEquals;
+	// equal blocks of the current iteration, to be recalculated before next step
+	// to track line number (current target to ultimate target) mapping 
+	private RangePairSeq intermediateEquals = new RangePairSeq();
+
+	private boolean[] knownLines;
+	private RevisionDescriptor revisionDescriptor;
+	private BlockData lineContent;
+
+	private IntMap<RangePairSeq> mergedRanges = new IntMap<RangePairSeq>(10);
+	private IntMap<RangePairSeq> equalRanges = new IntMap<RangePairSeq>(10);
+	private boolean activeEqualsComesFromMerge = false;
+
+	private int[] lineRevisions;
+
+	/**
+	 * @return desired order of iteration for diff
+	 */
+	public HgIterateDirection iterateDirection() {
+		return HgIterateDirection.NewToOld;
+	}
+
+	public void report(int annotateRevIndex, HgAnnotateCommand.Inspector insp, ProgressSupport progress, CancelSupport cancel) throws HgCallbackTargetException, CancelledException {
+		LineImpl li = new LineImpl();
+		progress.start(lineRevisions.length);
+		for (int i = 0; i < lineRevisions.length; i++) {
+			byte[] c = lineContent.elementAt(i).asArray();
+			li.init(i+1, lineRevisions[i], c);
+			insp.next(li);
+			progress.worked(1);
+			cancel.checkCancelled();
+		}
+		progress.done();
+	}
+
+	public void start(RevisionDescriptor rd) {
+		revisionDescriptor = rd;
+		if (knownLines == null) {
+			lineContent = rd.target();
+			knownLines = new boolean[lineContent.elementCount()];
+			lineRevisions = new int [lineContent.elementCount()];
+			Arrays.fill(lineRevisions, NO_REVISION);
+			activeEquals = new RangePairSeq();
+			activeEquals.add(0, 0, knownLines.length);
+			equalRanges.put(rd.targetChangesetIndex(), activeEquals);
+		} else {
+			activeEquals = equalRanges.get(rd.targetChangesetIndex());
+			if (activeEquals == null) {
+				// we didn't see this target revision as origin yet
+				// the only way this may happen is that this revision was a merge parent
+				activeEquals = mergedRanges.get(rd.targetChangesetIndex());
+				activeEqualsComesFromMerge = true;
+				if (activeEquals == null) {
+					throw new HgInvalidStateException(String.format("Can't find previously visited revision %d (while in %d->%1$d diff)", rd.targetChangesetIndex(), rd.originChangesetIndex()));
+				}
+			}
+		}
+	}
+
+	public void done(RevisionDescriptor rd) {
+		// update line numbers of the intermediate target to point to ultimate target's line numbers
+		RangePairSeq v = intermediateEquals.intersect(activeEquals);
+		if (activeEqualsComesFromMerge) {
+			mergedRanges.put(rd.originChangesetIndex(), v);
+		} else {
+			equalRanges.put(rd.originChangesetIndex(), v);
+		}
+		if (rd.isMerge() && !mergedRanges.containsKey(rd.mergeChangesetIndex())) {
+			// seen merge, but no lines were merged from p2.
+			// Add empty range to avoid uncertainty when a parent of p2 pops in
+			mergedRanges.put(rd.mergeChangesetIndex(), new RangePairSeq());
+		}
+		intermediateEquals.clear();
+		activeEquals = null;
+		activeEqualsComesFromMerge = false;
+		revisionDescriptor = null;
+	}
+
+	public void same(EqualBlock block) {
+		intermediateEquals.add(block.originStart(), block.targetStart(), block.length());
+	}
+
+	public void added(AddBlock block) {
+		RangePairSeq rs = null;
+		if (revisionDescriptor.isMerge() && block.originChangesetIndex() == revisionDescriptor.mergeChangesetIndex()) {
+			rs = mergedRanges.get(revisionDescriptor.mergeChangesetIndex());
+			if (rs == null) {
+				mergedRanges.put(revisionDescriptor.mergeChangesetIndex(), rs = new RangePairSeq());
+			}
+		}
+		if (activeEquals.size() == 0) {
+			return;
+		}
+		for (int i = 0, ln = block.firstAddedLine(), x = block.totalAddedLines(); i < x; i++, ln++) {
+			int lnInFinal = activeEquals.mapLineIndex(ln);
+			if (lnInFinal != -1/* && !knownLines[lnInFinal]*/) {
+				if (rs != null) {
+					rs.add(block.insertedAt() + i, lnInFinal, 1);
+				} else {
+					line(lnInFinal, block.targetChangesetIndex());
+				}
+				knownLines[lnInFinal] = true;
+			}
+		}
+	}
+
+	public void changed(ChangeBlock block) {
+		added(block);
+	}
+
+	public void deleted(DeleteBlock block) {
+	}
+
+	private void line(int lineNumber, int changesetRevIndex) {
+		lineRevisions[lineNumber] = changesetRevIndex;
+	}
+}
\ No newline at end of file
--- a/test/org/tmatesoft/hg/test/TestBlame.java	Thu Jul 18 18:02:36 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestBlame.java	Thu Jul 18 18:03:51 2013 +0200
@@ -20,7 +20,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.tmatesoft.hg.core.HgIterateDirection.NewToOld;
 import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew;
-import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import java.io.ByteArrayOutputStream;
@@ -47,15 +46,17 @@
 import org.tmatesoft.hg.core.HgDiffCommand;
 import org.tmatesoft.hg.core.HgRepoFacade;
 import org.tmatesoft.hg.core.Nodeid;
-import org.tmatesoft.hg.internal.FileAnnotation;
-import org.tmatesoft.hg.internal.FileAnnotation.LineDescriptor;
-import org.tmatesoft.hg.internal.FileAnnotation.LineInspector;
+import org.tmatesoft.hg.internal.ForwardAnnotateInspector;
 import org.tmatesoft.hg.internal.IntVector;
+import org.tmatesoft.hg.internal.ReverseAnnotateInspector;
 import org.tmatesoft.hg.repo.HgChangelog;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgLookup;
 import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.util.CancelSupport;
+import org.tmatesoft.hg.util.CancelledException;
 import org.tmatesoft.hg.util.Path;
+import org.tmatesoft.hg.util.ProgressSupport;
 
 /**
  * 
@@ -103,10 +104,11 @@
 			/*, TIP */};
 		for (int cs : toTest) {
 			ar.run(cs, false);
-			FileAnnotateInspector fa = new FileAnnotateInspector();
 			diffCmd.range(0, cs);
-			diffCmd.executeAnnotate(new FileAnnotation(fa));
-			doAnnotateLineCheck(cs, ar.getLines(), Arrays.asList(fa.lineRevisions), Arrays.asList(fa.lines));
+			final ReverseAnnotateInspector insp = new ReverseAnnotateInspector();
+			diffCmd.executeAnnotate(insp);
+			AnnotateInspector fa = new AnnotateInspector().fill(cs, insp);
+			doAnnotateLineCheck(cs, ar.getLines(), fa.changesets, fa.lines);
 		}
 	}
 	
@@ -119,10 +121,11 @@
 		final HgDiffCommand diffCmd = new HgDiffCommand(repo).file(df).order(NewToOld);
 		for (int cs : new int[] { 4, 6 /*, 8 see below*/, TIP}) {
 			ar.run(cs, false);
-			FileAnnotateInspector fa = new FileAnnotateInspector();
 			diffCmd.range(0, cs);
-			diffCmd.executeAnnotate(new FileAnnotation(fa));
-			doAnnotateLineCheck(cs, ar.getLines(), Arrays.asList(fa.lineRevisions), Arrays.asList(fa.lines));
+			final ReverseAnnotateInspector insp = new ReverseAnnotateInspector();
+			diffCmd.executeAnnotate(insp);
+			AnnotateInspector fa = new AnnotateInspector().fill(cs, insp);
+			doAnnotateLineCheck(cs, ar.getLines(), fa.changesets, fa.lines);
 		}
 		/*`hg annotate -r 8` and HgBlameFacility give different result
 		 * for "r0, line 5" line, which was deleted in rev2 and restored back in
@@ -131,7 +134,7 @@
 		 * However `hg annotate -r 4` shows rev4 for the line, too. The aforementioned rev0 for 
 		 * the merge rev8 results from the iteration order and is implementation specific 
 		 * (i.e. one can't tell which one is right). Mercurial walks from parents to children,
-		 * and traces equal lines, wile HgBlameFacility walks from child to parents and records 
+		 * and traces equal lines, while HgBlameFacility walks from child to parents and records 
 		 * changes (additions). Seems it processes branch with rev3 and rev6 first 
 		 * (printout in context.py, annotate and annotate.pair reveals that), and the line 0_5
 		 * comes as unchanged through this branch, and later processing rev2 and rev4 doesn't 
@@ -259,6 +262,8 @@
 		ar.run(changeset, false);
 		doAnnotateLineCheck(changeset, ar.getLines(), ai.changesets, ai.lines);
 	}
+	
+	// FIXME add originLineNumber to HgAnnotateCommand#LineInfo, pass it from FileAnnotate, test
 
 	private void doAnnotateLineCheck(int cs, String[] hgAnnotateLines, List<Integer> cmdChangesets, List<String> cmdLines) {
 		assertTrue("[sanity]", hgAnnotateLines.length > 0);
@@ -367,11 +372,13 @@
 		}
 		errorCollector.verify();
 		*/
-		FileAnnotateInspector fa = new FileAnnotateInspector();
-		diffCmd.range(0, 8).order(NewToOld);
-		diffCmd.executeAnnotate(new FileAnnotation(fa));
-		for (int i = 0; i < fa.lineRevisions.length; i++) {
-			System.out.printf("%d: %s", fa.lineRevisions[i], fa.line(i) == null ? "null\n" : fa.line(i));
+		ForwardAnnotateInspector insp = new ForwardAnnotateInspector();
+		diffCmd.range(0, 8).order(insp.iterateDirection());
+		diffCmd.executeAnnotate(insp);
+		AnnotateInspector fa = new AnnotateInspector().fill(8, insp);
+		for (int i = 0; i < fa.changesets.size(); i++) {
+			final String line = fa.lines.get(i);
+			System.out.printf("%d: %s", fa.changesets.get(i), line == null ? "null\n" : line);
 		}
 	}
 
@@ -476,28 +483,6 @@
 		}
 	}
 
-	private static class FileAnnotateInspector implements LineInspector {
-		private Integer[] lineRevisions;
-		private String[] lines;
-		
-		FileAnnotateInspector() {
-		}
-		
-		public void line(int lineNumber, int changesetRevIndex, HgBlameInspector.BlockData lineContent, LineDescriptor ld) {
-			if (lineRevisions == null) {
-				lineRevisions = new Integer[ld.totalLines()];
-				Arrays.fill(lineRevisions, NO_REVISION);
-				lines = new String[ld.totalLines()];
-			}
-			lineRevisions[lineNumber] = changesetRevIndex;
-			lines[lineNumber] = new String(lineContent.asArray());
-		}
-		
-		public String line(int i) {
-			return lines[i];
-		}
-	}
-
 	@SuppressWarnings("unused")
 	private static class LineDumpInspector implements HgBlameInspector {
 		
@@ -544,7 +529,16 @@
 		private int lineNumber = 1;
 		public final ArrayList<String> lines = new ArrayList<String>();
 		public final ArrayList<Integer> changesets = new ArrayList<Integer>();
-		
+
+		AnnotateInspector fill(int rev, ReverseAnnotateInspector ai) throws HgCallbackTargetException, CancelledException {
+			ai.report(rev, this, ProgressSupport.Factory.get(null), CancelSupport.Factory.get(null));
+			return this;
+		}
+		AnnotateInspector fill(int rev, ForwardAnnotateInspector ai) throws HgCallbackTargetException, CancelledException {
+			ai.report(rev, this, ProgressSupport.Factory.get(null), CancelSupport.Factory.get(null));
+			return this;
+		}
+
 		public void next(LineInfo lineInfo) throws HgCallbackTargetException {
 			Assert.assertEquals(lineInfo.getLineNumber(), lineNumber);
 			lineNumber++;