changeset 703:7839ff0bfd78

Refactor: move diff/blame related code to a separate package
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 14 Aug 2013 14:51:51 +0200
parents 992fa84e7885
children 7743a9c10bfa
files src/org/tmatesoft/hg/core/HgAnnotateCommand.java src/org/tmatesoft/hg/core/HgDiffCommand.java src/org/tmatesoft/hg/internal/BlameHelper.java src/org/tmatesoft/hg/internal/DiffHelper.java src/org/tmatesoft/hg/internal/ForwardAnnotateInspector.java src/org/tmatesoft/hg/internal/GeneratePatchInspector.java src/org/tmatesoft/hg/internal/LineImpl.java src/org/tmatesoft/hg/internal/RangePairSeq.java src/org/tmatesoft/hg/internal/ReverseAnnotateInspector.java src/org/tmatesoft/hg/internal/diff/BlameHelper.java src/org/tmatesoft/hg/internal/diff/DiffHelper.java src/org/tmatesoft/hg/internal/diff/DiffRangeMap.java src/org/tmatesoft/hg/internal/diff/ForwardAnnotateInspector.java src/org/tmatesoft/hg/internal/diff/LineImpl.java src/org/tmatesoft/hg/internal/diff/RangePairSeq.java src/org/tmatesoft/hg/internal/diff/ReverseAnnotateInspector.java test/org/tmatesoft/hg/test/TestAuxUtilities.java test/org/tmatesoft/hg/test/TestBlame.java test/org/tmatesoft/hg/test/TestDiffHelper.java test/org/tmatesoft/hg/test/TestRevlog.java
diffstat 20 files changed, 1969 insertions(+), 1952 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgAnnotateCommand.java	Thu Aug 08 21:32:22 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgAnnotateCommand.java	Wed Aug 14 14:51:51 2013 +0200
@@ -20,7 +20,7 @@
 
 import org.tmatesoft.hg.internal.Callback;
 import org.tmatesoft.hg.internal.CsetParamKeeper;
-import org.tmatesoft.hg.internal.ForwardAnnotateInspector;
+import org.tmatesoft.hg.internal.diff.ForwardAnnotateInspector;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgRuntimeException;
--- a/src/org/tmatesoft/hg/core/HgDiffCommand.java	Thu Aug 08 21:32:22 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgDiffCommand.java	Wed Aug 14 14:51:51 2013 +0200
@@ -19,10 +19,10 @@
 import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
-import org.tmatesoft.hg.internal.BlameHelper;
 import org.tmatesoft.hg.internal.CsetParamKeeper;
 import org.tmatesoft.hg.internal.FileHistory;
 import org.tmatesoft.hg.internal.FileRevisionHistoryChunk;
+import org.tmatesoft.hg.internal.diff.BlameHelper;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgRuntimeException;
--- a/src/org/tmatesoft/hg/internal/BlameHelper.java	Thu Aug 08 21:32:22 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,967 +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 static org.tmatesoft.hg.core.HgIterateDirection.OldToNew;
-import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.ListIterator;
-
-import org.tmatesoft.hg.core.HgCallbackTargetException;
-import org.tmatesoft.hg.core.Nodeid;
-import org.tmatesoft.hg.internal.DiffHelper.LineSequence;
-import org.tmatesoft.hg.internal.DiffHelper.LineSequence.ByteChain;
-import org.tmatesoft.hg.internal.diff.DiffRangeMap;
-import org.tmatesoft.hg.internal.diff.DiffRangeMap.RangePair;
-import org.tmatesoft.hg.core.HgBlameInspector;
-import org.tmatesoft.hg.core.HgBlameInspector.*;
-import org.tmatesoft.hg.repo.HgChangelog;
-import org.tmatesoft.hg.repo.HgDataFile;
-import org.tmatesoft.hg.repo.HgInvalidStateException;
-import org.tmatesoft.hg.repo.HgParentChildMap;
-import org.tmatesoft.hg.repo.HgRepository;
-import org.tmatesoft.hg.repo.HgRevisionMap;
-import org.tmatesoft.hg.repo.HgRuntimeException;
-import org.tmatesoft.hg.util.Adaptable;
-import org.tmatesoft.hg.util.CancelledException;
-import org.tmatesoft.hg.util.Pair;
-
-/**
- * Blame implementation
- * @see HgBlameInspector
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-public class BlameHelper {
-	
-	private final HgBlameInspector insp;
-	private FileLinesCache linesCache;
-	private HgParentChildMap<HgChangelog> clogMap;
-
-	public BlameHelper(HgBlameInspector inspector) {
-		insp = inspector;
-	}
-
-	/**
-	 * Build history of the file for the specified range (follow renames if necessary). This history
-	 * is used to access various file revision data during subsequent {@link #diff(int, int, int, int)} and
-	 * {@link #annotateChange(int, int, int[], int[])} calls. Callers can use returned history for own approaches 
-	 * to iteration over file history.
-
-	 * <p>NOTE, clogRevIndexEnd has to list name of the supplied file in the corresponding manifest,
-	 * as it's not possible to trace rename history otherwise.
-	 */
-	public FileHistory prepare(HgDataFile df, int clogRevIndexStart, int clogRevIndexEnd) throws HgRuntimeException {
-		assert clogRevIndexStart <= clogRevIndexEnd;
-		FileHistory fileHistory = new FileHistory(df, clogRevIndexStart, clogRevIndexEnd);
-		fileHistory.build();
-		int cacheHint = 5; // cache comes useful when we follow merge branches and don't want to
-		// parse base revision twice. There's no easy way to determine max(distance(all(base,merge))),
-		// hence the heuristics to use the longest history chunk:
-		for (FileRevisionHistoryChunk c : fileHistory.iterate(OldToNew)) {
-			// iteration order is not important here
-			if (c.revisionCount() > cacheHint) {
-				cacheHint = c.revisionCount();
-			}
-		}
-		linesCache = new FileLinesCache(cacheHint);
-		for (FileRevisionHistoryChunk fhc : fileHistory.iterate(OldToNew)) {
-			// iteration order is not important here
-			linesCache.useFileUpTo(fhc.getFile(), fhc.getEndChangeset());
-		}
-		return fileHistory;
-	}
-	
-	// NO_REVISION is not allowed as any argument
-	public void diff(int fileRevIndex1, int clogRevIndex1, int fileRevIndex2, int clogRevIndex2) throws HgCallbackTargetException, HgRuntimeException {
-		HgDataFile targetFile = linesCache.getFile(clogRevIndex2);
-		LineSequence c1 = linesCache.lines(clogRevIndex1, fileRevIndex1);
-		LineSequence c2 = linesCache.lines(clogRevIndex2, fileRevIndex2);
-		DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>();
-		pg.init(c1, c2);
-		BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex2, insp, clogRevIndex1, clogRevIndex2);
-		pg.findMatchingBlocks(bbi);
-		bbi.checkErrors();
-	}
-
-	public void annotateChange(int fileRevIndex, int csetRevIndex, int[] fileParentRevs, int[] fileParentClogRevs) throws HgCallbackTargetException, HgRuntimeException {
-		HgDataFile targetFile = linesCache.getFile(csetRevIndex);
-		final LineSequence fileRevLines = linesCache.lines(csetRevIndex, fileRevIndex);
-		if (fileParentClogRevs[0] != NO_REVISION && fileParentClogRevs[1] != NO_REVISION) {
-			int p1ClogIndex = fileParentClogRevs[0];
-			int p2ClogIndex = fileParentClogRevs[1];
-			LineSequence p1Lines = linesCache.lines(p1ClogIndex, fileParentRevs[0]);
-			LineSequence p2Lines = linesCache.lines(p2ClogIndex, fileParentRevs[1]);
-			MergeResolutionStrategy mergeResolver = createMergeStrategy(fileRevLines, p1Lines, p2Lines, csetRevIndex, fileParentClogRevs);
-			//
-			DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>();
-			pg.init(p1Lines, fileRevLines);
-			BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex, insp, p1ClogIndex, csetRevIndex);
-			bbi.setMergeParent2(mergeResolver, p2ClogIndex);
-			pg.findMatchingBlocks(bbi);
-			bbi.checkErrors();
-		} else if (fileParentClogRevs[0] == fileParentClogRevs[1]) {
-			// may be equal iff both are unset
-			assert fileParentClogRevs[0] == NO_REVISION;
-			// everything added
-			BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex, insp, NO_REVISION, csetRevIndex);
-			bbi.begin(LineSequence.newlines(new byte[0]), fileRevLines);
-			bbi.match(0, fileRevLines.chunkCount()-1, 0);
-			bbi.end();
-			bbi.checkErrors();
-		} else {
-			int soleParentIndex = fileParentClogRevs[0] == NO_REVISION ? 1 : 0;
-			assert fileParentClogRevs[soleParentIndex] != NO_REVISION;
-			LineSequence parentLines = linesCache.lines(fileParentClogRevs[soleParentIndex], fileParentRevs[soleParentIndex]);
-			
-			DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>();
-			pg.init(parentLines, fileRevLines);
-			BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex, insp, fileParentClogRevs[soleParentIndex], csetRevIndex);
-			pg.findMatchingBlocks(bbi);
-			bbi.checkErrors();
-		}
-	}
-	
-	private static final boolean useNewStrategy = Boolean.TRUE.booleanValue();
-	
-	private MergeResolutionStrategy createMergeStrategy(LineSequence fileRevLines, LineSequence p1Lines, LineSequence p2Lines, int csetRevIndex, int[] fileParentClogRevs) {
-		DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>();
-		if (useNewStrategy) {
-			final ArrayList<RangePairSeq> allMatches = new ArrayList<RangePairSeq>();
-			pg.init(p2Lines, fileRevLines);
-			pg.findAllMatchAlternatives(new DiffHelper.MatchInspector<LineSequence>() {
-				private RangePairSeq matches;
-
-				public void begin(LineSequence s1, LineSequence s2) {
-					matches = new RangePairSeq();
-				}
-
-				public void match(int startSeq1, int startSeq2, int matchLength) {
-					matches.add(startSeq1, startSeq2, matchLength);
-				}
-
-				public void end() {
-					if (matches.size() > 0) {
-						allMatches.add(matches);
-					}
-				}
-				
-			});
-			//
-			LineSequence baseLines = getBaseRevisionLines(csetRevIndex, fileParentClogRevs);
-			pg.init(p1Lines, baseLines);
-			DiffRangeMap p1ToBase = new DiffRangeMap().fill(pg);
-			pg.init(baseLines, p2Lines);
-			DiffRangeMap baseToP2 = new DiffRangeMap().fill(pg);
-			return new MergeStrategy2(allMatches, p1ToBase, baseToP2);
-		} else {
-			pg.init(p2Lines, fileRevLines);
-			EqualBlocksCollector p2MergeCommon = new EqualBlocksCollector();
-			pg.findMatchingBlocks(p2MergeCommon);
-			return new MergeStrategy1(p2MergeCommon.matches);
-		}
-	}
-
-	private LineSequence getBaseRevisionLines(int clogRevIndex, int[] fileParentClogRevs) {
-		assert fileParentClogRevs[0] >= 0;
-		assert fileParentClogRevs[1] >= 0;
-		HgDataFile targetFile = linesCache.getFile(clogRevIndex);
-		final HgRepository repo = targetFile.getRepo();
-		if (clogMap == null) {
-			// FIXME replace HgParentChildMap with revlog.indexWalk(AncestorIterator))
-			clogMap = new HgParentChildMap<HgChangelog>(repo.getChangelog());
-			clogMap.init();
-		}
-		final HgRevisionMap<HgChangelog> m = clogMap.getRevisionMap();
-		Nodeid ancestor = clogMap.ancestor(m.revision(fileParentClogRevs[0]), m.revision(fileParentClogRevs[1]));
-		final int ancestorRevIndex = m.revisionIndex(ancestor);
-		Nodeid fr = repo.getManifest().getFileRevision(ancestorRevIndex, targetFile.getPath());
-		if (fr == null) {
-			return LineSequence.newlines(new byte[0]);
-		}
-		return linesCache.lines(ancestorRevIndex, targetFile.getRevisionIndex(fr));
-	}
-
-	private static class FileLinesCache {
-		private final LinkedList<Pair<Integer, LineSequence>> lruCache;
-		private final int limit;
-		private final LinkedList<Pair<Integer, HgDataFile>> files; // TODO in fact, need sparse array 
-
-		/**
-		 * @param lruLimit how many parsed file revisions to keep
-		 */
-		public FileLinesCache(int lruLimit) {
-			limit = lruLimit;
-			lruCache = new LinkedList<Pair<Integer, LineSequence>>();
-			files = new LinkedList<Pair<Integer,HgDataFile>>();
-		}
-		
-		public void useFileUpTo(HgDataFile df, int clogRevIndex) {
-			Pair<Integer, HgDataFile> newEntry = new Pair<Integer, HgDataFile>(clogRevIndex, df);
-			for (ListIterator<Pair<Integer, HgDataFile>> it = files.listIterator(); it.hasNext();) {
-				Pair<Integer, HgDataFile> e = it.next();
-				if (e.first() == clogRevIndex) {
-					assert e.second().getPath().equals(df.getPath());
-					return;
-				}
-				if (e.first() > clogRevIndex) {
-					// insert new entry before current
-					it.previous();
-					it.add(newEntry);
-					return;
-				}
-			}
-			files.add(newEntry);
-		}
-		
-		public HgDataFile getFile(int clogRevIndex) {
-			for (Pair<Integer, HgDataFile> e : files) {
-				if (e.first() >= clogRevIndex) {
-					return e.second();
-				}
-			}
-			throw new HgInvalidStateException(String.format("Got %d file-changelog mappings, but no luck for revision %d.", files.size(), clogRevIndex));
-		}
-
-		public LineSequence lines(int clogRevIndex, int fileRevIndex) throws HgRuntimeException {
-			Pair<Integer, LineSequence> cached = checkCache(clogRevIndex);
-			if (cached != null) {
-				return cached.second();
-			}
-			HgDataFile df = getFile(clogRevIndex);
-			try {
-				ByteArrayChannel c;
-				df.content(fileRevIndex, c = new ByteArrayChannel());
-				LineSequence rv = LineSequence.newlines(c.toArray());
-				lruCache.addFirst(new Pair<Integer, LineSequence>(clogRevIndex, rv));
-				if (lruCache.size() > limit) {
-					lruCache.removeLast();
-				}
-				return rv;
-			} catch (CancelledException ex) {
-				// TODO likely it was bad idea to throw cancelled exception from content()
-				// deprecate and provide alternative?
-				HgInvalidStateException ise = new HgInvalidStateException("ByteArrayChannel never throws CancelledException");
-				ise.initCause(ex);
-				throw ise;
-			}
-		}
-		
-		private Pair<Integer,LineSequence> checkCache(int fileRevIndex) {
-			Pair<Integer, LineSequence> rv = null;
-			for (ListIterator<Pair<Integer, LineSequence>> it = lruCache.listIterator(); it.hasNext(); ) {
-				Pair<Integer, LineSequence> p = it.next();
-				if (p.first() == fileRevIndex) {
-					rv = p;
-					it.remove();
-					break;
-				}
-			}
-			if (rv != null) {
-				lruCache.addFirst(rv);
-			}
-			return rv;
-		}
-	}
-
-	private static class BlameBlockInspector extends DiffHelper.DeltaInspector<LineSequence> {
-		private final HgBlameInspector insp;
-		private final int csetOrigin;
-		private final int csetTarget;
-		private MergeResolutionStrategy p2MergeCommon;
-		private int csetMergeParent;
-		private final AnnotateRev annotatedRevision;
-		private HgCallbackTargetException error;
-
-		public BlameBlockInspector(HgDataFile df, int fileRevIndex, HgBlameInspector inspector, int originCset, int targetCset) {
-			assert inspector != null;
-			insp = inspector;
-			annotatedRevision = new AnnotateRev();
-			annotatedRevision.set(df, fileRevIndex);
-			csetOrigin = originCset;
-			csetTarget = targetCset;
-		}
-		
-		public void setMergeParent2(MergeResolutionStrategy p2MergeStrategy, int parentCset2) {
-			p2MergeCommon = p2MergeStrategy;
-			csetMergeParent = parentCset2;
-		}
-		
-		@Override
-		public void begin(LineSequence s1, LineSequence s2) {
-			super.begin(s1, s2);
-			if (shallStop()) {
-				return;
-			}
-			ContentBlock originContent = new ContentBlock(s1);
-			ContentBlock targetContent = new ContentBlock(s2);
-			annotatedRevision.set(originContent, targetContent);
-			annotatedRevision.set(csetOrigin, csetTarget, p2MergeCommon != null ? csetMergeParent : NO_REVISION);
-			RevisionDescriptor.Recipient curious = Adaptable.Factory.getAdapter(insp, RevisionDescriptor.Recipient.class, null);
-			if (curious != null) {
-				try {
-					curious.start(annotatedRevision);
-				} catch (HgCallbackTargetException ex) {
-					error = ex;
-				}
-			}
-		}
-		
-		@Override
-		public void end() {
-			super.end();
-			if (shallStop()) {
-				return;
-			}
-			RevisionDescriptor.Recipient curious = Adaptable.Factory.getAdapter(insp, RevisionDescriptor.Recipient.class, null);
-			if (curious != null) {
-				try {
-					curious.done(annotatedRevision);
-				} catch (HgCallbackTargetException ex) {
-					error = ex;
-				}
-			}
-			p2MergeCommon = null;
-		}
-
-		@Override
-		protected void changed(int s1From, int s1To, int s2From, int s2To) {
-			if (shallStop()) {
-				return;
-			}
-			try {
-				if (p2MergeCommon != null) {
-					IntSliceSeq mergeRanges = p2MergeCommon.combineAndMarkRangesWithSource(s1From, s1To, s2From, s2To, csetOrigin, csetMergeParent);
-					
-					/*
-					 * Usecases, how it USED TO BE initially:
-					 * 3 lines changed to 10 lines. range of 10 lines breaks down to 2 from p2, 3 from p1, and 5 from p2.
-					 * We report: 2 lines changed to 2(p2), then 1 line changed with 3(p1) and 5 lines added from p2.
-					 * 
-					 * 10 lines changed to 3 lines, range of 3 lines breaks down to 2 line from p1 and 1 line from p2.
-					 * We report: 2 lines changed to 2(p1) and 8 lines changed to 1(p2)
-					 * 
-					 * NOW, lines from p2 are always reported as pure add (since we need their insertion point to be in p2, not in p1)
-					 * and we try to consume p1 changes as soon as we see first p1's range 
-					 */
-					int s1TotalLines = s1To - s1From, s1ConsumedLines = 0, s1Start = s1From;
-					
-					for (Iterator<IntTuple> it = mergeRanges.iterator(); it.hasNext();) {
-						IntTuple mergeRange = it.next();
-						final int rangeOrigin = mergeRange.at(0);
-						final int rangeStart = mergeRange.at(1);
-						final int rangeLen = mergeRange.at(2);
-						final boolean lastRange = it.hasNext();
-						final int s1LinesLeft = s1TotalLines - s1ConsumedLines;
-						// how many lines we may report as changed (don't use more than in range unless it's the very last range)
-						final int s1LinesToBorrow = lastRange ? s1LinesLeft : Math.min(s1LinesLeft, rangeLen);
-						if (rangeOrigin != csetMergeParent && s1LinesToBorrow > 0) {
-							ChangeBlockImpl block = getChangeBlock(s1Start, s1LinesToBorrow, rangeStart, rangeLen);
-							block.setOriginAndTarget(rangeOrigin, csetTarget);
-							insp.changed(block);
-							s1ConsumedLines += s1LinesToBorrow;
-							s1Start += s1LinesToBorrow;
-						} else {
-							int blockInsPoint = rangeOrigin != csetMergeParent ? s1Start : p2MergeCommon.getLineInP2(rangeStart);
-							ChangeBlockImpl block = getAddBlock(rangeStart, rangeLen, blockInsPoint);
-							block.setOriginAndTarget(rangeOrigin, csetTarget);
-							insp.added(block);
-						}
-					}
-					if (s1ConsumedLines != s1TotalLines) {
-						assert s1ConsumedLines < s1TotalLines : String.format("Expected to process %d lines, but actually was %d", s1TotalLines, s1ConsumedLines);
-						// either there were no ranges from p1, whole s2From..s2To range came from p2, shall report as deleted
-						// or the ranges found were not enough to consume whole s2From..s2To
-						// The "deletion point" is shifted to the end of last csetOrigin->csetTarget change
-						int s2DeletePoint = s2From + s1ConsumedLines;
-						ChangeBlockImpl block =  new ChangeBlockImpl(annotatedRevision.origin, null, s1Start, s1To - s1Start, -1, -1, -1, s2DeletePoint);
-						block.setOriginAndTarget(csetOrigin, csetTarget);
-						insp.deleted(block);
-					}
-				} else {
-					ChangeBlockImpl block = getChangeBlock(s1From, s1To - s1From, s2From, s2To - s2From);
-					block.setOriginAndTarget(csetOrigin, csetTarget);
-					insp.changed(block);
-				}
-			} catch (HgCallbackTargetException ex) {
-				error = ex;
-			}
-		}
-		
-		@Override
-		protected void added(int s1InsertPoint, int s2From, int s2To) {
-			if (shallStop()) {
-				return;
-			}
-			try {
-				if (p2MergeCommon != null) {
-					IntSliceSeq mergeRanges = p2MergeCommon.combineAndMarkRangesWithSource(s1InsertPoint, s2From, s2To, csetOrigin, csetMergeParent);
-					int insPoint = s1InsertPoint; // track changes to insertion point
-					for (IntTuple mergeRange : mergeRanges) {
-						int rangeOrigin = mergeRange.at(0);
-						int rangeStart = mergeRange.at(1);
-						int rangeLen = mergeRange.at(2);
-						// XXX likely need somewhat similar to the code above: 
-						// int blockInsPoint = rangeOrigin != csetMergeParent ? s1Start : p2MergeCommon.reverseMapLine(rangeStart);
-						//
-						ChangeBlockImpl block = getAddBlock(rangeStart, rangeLen, insPoint);
-						block.setOriginAndTarget(rangeOrigin, csetTarget);
-						insp.added(block);
-						// indicate insPoint moved down number of lines we just reported
-						insPoint += rangeLen;
-					}
-				} else {
-					ChangeBlockImpl block = getAddBlock(s2From, s2To - s2From, s1InsertPoint);
-					block.setOriginAndTarget(csetOrigin, csetTarget);
-					insp.added(block);
-				}
-			} catch (HgCallbackTargetException ex) {
-				error = ex;
-			}
-		}
-		
-		@Override
-		protected void deleted(int s2DeletePoint, int s1From, int s1To) {
-			if (shallStop()) {
-				return;
-			}
-			try {
-				ChangeBlockImpl block = new ChangeBlockImpl(annotatedRevision.origin, null, s1From, s1To - s1From, -1, -1, -1, s2DeletePoint);
-				block.setOriginAndTarget(csetOrigin, csetTarget);
-				insp.deleted(block);
-			} catch (HgCallbackTargetException ex) {
-				error = ex;
-			}
-		}
-
-		@Override
-		protected void unchanged(int s1From, int s2From, int length) {
-			if (shallStop()) {
-				return;
-			}
-			try {
-				EqualBlockImpl block = new EqualBlockImpl(s1From, s2From, length, annotatedRevision.target);
-				block.setOriginAndTarget(csetOrigin, csetTarget);
-				insp.same(block);
-			} catch (HgCallbackTargetException ex) {
-				error = ex;
-			}
-		}
-		
-		void checkErrors() throws HgCallbackTargetException {
-			if (error != null) {
-				throw error;
-			}
-		}
-		
-		private boolean shallStop() {
-			return error != null;
-		}
-		
-		private ChangeBlockImpl getAddBlock(int start, int len, int insPoint) {
-			return new ChangeBlockImpl(null, annotatedRevision.target, -1, -1, start, len, insPoint, -1);
-		}
-		
-		private ChangeBlockImpl getChangeBlock(int start1, int len1, int start2, int len2) {
-			return new ChangeBlockImpl(annotatedRevision.origin, annotatedRevision.target, start1, len1, start2, len2, start1, start2);
-		}
-	}
-	
-	private static class BlockImpl implements Block {
-		private int originCset;
-		private int targetCset;
-
-		void setOriginAndTarget(int originChangesetIndex, int targetChangesetIndex) {
-			// XXX perhaps, shall be part of Inspector API, rather than Block's
-			// as they don't change between blocks (although the moment about merged revisions)
-			// is not yet clear to me
-			originCset = originChangesetIndex;
-			targetCset = targetChangesetIndex;
-		}
-
-		public int originChangesetIndex() {
-			return originCset;
-		}
-
-		public int targetChangesetIndex() {
-			return targetCset;
-		}
-	}
-
-	private static class EqualBlockImpl extends BlockImpl implements EqualBlock {
-		private final int start1, start2;
-		private final int length;
-		private final ContentBlock fullContent;
-		private FilterBlock myContent;
-		
-		EqualBlockImpl(int blockStartSeq1, int blockStartSeq2, int blockLength, ContentBlock targetContent) {
-			start1 = blockStartSeq1;
-			start2 = blockStartSeq2;
-			length = blockLength;
-			fullContent = targetContent;
-		}
-
-		public int originStart() {
-			return start1;
-		}
-
-		public int targetStart() {
-			return start2;
-		}
-
-		public int length() {
-			return length;
-		}
-		
-		public BlockData content() {
-			if (myContent == null) {
-				myContent = new FilterBlock(fullContent, start2, length);
-			}
-			return myContent;
-		}
-		
-		@Override
-		public String toString() {
-			return String.format("@@ [%d..%d) == [%d..%d) @@", start1, start1+length, start2, start2+length);
-		}
-	}
-	
-	private static class ChangeBlockImpl extends BlockImpl implements ChangeBlock {
-		private final ContentBlock oldContent;
-		private final ContentBlock newContent;
-		private final int s1Start;
-		private final int s1Len;
-		private final int s2Start;
-		private final int s2Len;
-		private final int s1InsertPoint;
-		private final int s2DeletePoint;
-		private FilterBlock addedBlock, removedBlock;
-
-		public ChangeBlockImpl(ContentBlock c1, ContentBlock c2, int s1Start, int s1Len, int s2Start, int s2Len, int s1InsertPoint, int s2DeletePoint) {
-			oldContent = c1;
-			newContent = c2;
-			this.s1Start = s1Start;
-			this.s1Len = s1Len;
-			this.s2Start = s2Start;
-			this.s2Len = s2Len;
-			this.s1InsertPoint = s1InsertPoint;
-			this.s2DeletePoint = s2DeletePoint;
-		}
-		
-		public int insertedAt() {
-			return s1InsertPoint;
-		}
-
-		public int firstAddedLine() {
-			return s2Start;
-		}
-
-		public int totalAddedLines() {
-			return s2Len;
-		}
-
-		public BlockData addedLines() {
-			if (addedBlock == null) {
-				addedBlock = new FilterBlock(newContent, firstAddedLine(), totalAddedLines());
-			}
-			return addedBlock;
-		}
-		
-		public int removedAt() {
-			return s2DeletePoint;
-		}
-
-		public int firstRemovedLine() {
-			return s1Start;
-		}
-
-		public int totalRemovedLines() {
-			return s1Len;
-		}
-
-		public BlockData removedLines() {
-			if (removedBlock == null) {
-				removedBlock = new FilterBlock(oldContent, firstRemovedLine(), totalRemovedLines());
-			}
-			return removedBlock;
-		}
-		
-		@Override
-		public String toString() {
-			if (s2DeletePoint == -1) {
-				return String.format("@@ -%d,0 +%d,%d @@", insertedAt(), firstAddedLine(), totalAddedLines());
-			} else if (s1InsertPoint == -1) {
-				// delete only
-				return String.format("@@ -%d,%d +%d,0 @@", firstRemovedLine(), totalRemovedLines(), removedAt());
-			}
-			return String.format("@@ -%d,%d +%d,%d @@", firstRemovedLine(), totalRemovedLines(), firstAddedLine(), totalAddedLines());
-		}
-	}
-	
-	private static class SingleLine implements BlockData {
-		private final ByteChain line;
-
-		public SingleLine(ByteChain lineContent) {
-			line = lineContent;
-		}
-
-		public BlockData elementAt(int index) {
-			assert false;
-			return null;
-		}
-
-		public int elementCount() {
-			return 0;
-		}
-
-		public byte[] asArray() {
-			return line.data();
-		}
-	}
-	
-	private static class ContentBlock implements BlockData {
-		private final LineSequence seq;
-
-		public ContentBlock(LineSequence sequence) {
-			seq = sequence;
-		}
-
-		public BlockData elementAt(int index) {
-			return new SingleLine(seq.chunk(index));
-		}
-
-		public int elementCount() {
-			return seq.chunkCount() - 1;
-		}
-
-		public byte[] asArray() {
-			return seq.data(0, seq.chunkCount() - 1);
-		}
-	}
-	
-	private static class FilterBlock implements BlockData {
-		private final ContentBlock contentBlock;
-		private final int from;
-		private final int length;
-
-		public FilterBlock(ContentBlock bd, int startFrom, int len) {
-			assert bd != null;
-			assert startFrom + len < bd.seq.chunkCount(); // there's one extra chunk in the end, so strict less is ok
-			contentBlock = bd;
-			from = startFrom;
-			length = len;
-		}
-
-		public BlockData elementAt(int index) {
-			if (index < 0 || index >= length) {
-				throw new IllegalArgumentException(String.format("Expected value from [0..%d), got %d", length, index));
-			}
-			return contentBlock.elementAt(from + index);
-		}
-
-		public int elementCount() {
-			return length;
-		}
-
-		public byte[] asArray() {
-			return contentBlock.seq.data(from, from + length);
-		}
-	}
-
-	private static class EqualBlocksCollector implements DiffHelper.MatchInspector<LineSequence> {
-		private final RangePairSeq matches = new RangePairSeq();
-
-		public void begin(LineSequence s1, LineSequence s2) {
-		}
-
-		public void match(int startSeq1, int startSeq2, int matchLength) {
-			matches.add(startSeq1, startSeq2, matchLength);
-		}
-
-		public void end() {
-		}
-
-		public void intersectWithTarget(int start, int length, IntVector result) {
-			int s = start;
-			for (int l = start, x = start + length; l < x; l++) {
-				if (!matches.includesTargetLine(l)) {
-					if (l - s > 0) {
-						result.add(s);
-						result.add(l - s);
-					}
-					s = l+1;
-				}
-			}
-			if (s < start+length) {
-				result.add(s);
-				result.add((start + length) - s);
-			}
-		}
-		
-	}
-
-	interface MergeResolutionStrategy {
-		/**
-		 * breaks region [start2..end2) into ranges according to deduced (or simply guessed)
-		 * matching of [start1..end1) lines to lines in source1 and source2
-		 * @return list of tuples (source, start, length), where source is one of the identifiers supplied
-		 */
-		public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2);
-		public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2);
-		public int getLineInP2(int mergeLine);
-	}
-
-	// report lines as merged from p2 solely based on whether target line belongs
-	// to a region that is equal to p2 region
-	private static class MergeStrategy1 implements MergeResolutionStrategy {
-		// equal ranges in p2 and merged revision
-		private final RangePairSeq matches; 
-		private final IntSliceSeq mergeRanges;
-
-		public MergeStrategy1(RangePairSeq p2EqualToM) {
-			matches = p2EqualToM;
-			mergeRanges = new IntSliceSeq(3, 10, 10);
-		}
-
-		/*
-		 * intersects [start..start+length) with ranges of target lines, and based on the intersection 
-		 * breaks initial range into smaller ranges and records them into result, with marker to indicate
-		 * whether the range is from initial range (markerSource) or is a result of the intersection with target
-		 * (markerTarget)
-		 */
-		private IntSliceSeq doCombine(int start, int length, int markerSource, int markerTarget) {
-			mergeRanges.clear();
-			assert mergeRanges.sliceSize() == 3;
-			int sourceStart = start, targetStart = start, sourceEnd = start + length;
-			for (int l = sourceStart; l < sourceEnd; l++) {
-				if (matches.includesTargetLine(l)) {
-					// l is from target
-					if (sourceStart < l) {
-						// few lines from source range were not in the target, report them
-						mergeRanges.add(markerSource, sourceStart, l - sourceStart);
-					}
-					// indicate the earliest line from source range to use
-					sourceStart = l + 1;
-				} else {
-					// l is not in target
-					if (targetStart < l) {
-						// report lines from target range
-						mergeRanges.add(markerTarget, targetStart, l - targetStart);
-					}
-					// next line *may* be from target
-					targetStart = l + 1;
-				}
-			}
-			// if source range end with line from target, sourceStart would be == sourceEnd, and we need to add range with markerTarget
-			// if source range doesn't end with target line, targetStart == sourceEnd, while sourceStart < sourceEnd
-			if (sourceStart < sourceEnd) {
-				assert targetStart == sourceEnd;
-				// something left from the source range
-				mergeRanges.add(markerSource, sourceStart, sourceEnd - sourceStart);
-			} else if (targetStart < sourceEnd) {
-				assert sourceStart == sourceEnd;
-				mergeRanges.add(markerTarget, targetStart, sourceEnd - targetStart);
-			}
-			return mergeRanges;
-		}
-
-		public int getLineInP2(int mergeLine) {
-			return matches.reverseMapLine(mergeLine);
-		}
-
-		public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2) {
-			return doCombine(start2, end2 - start2, source1, source2);
-		}
-
-		public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2) {
-			return doCombine(start, end - start, source1, source2);
-		}
-	}
-	
-	private static class MergeStrategy2 implements MergeResolutionStrategy {
-		// equal ranges in p2 and merged revision
-		private final List<RangePairSeq> matches; 
-		private final IntSliceSeq mergeRanges;
-		private final DiffRangeMap p1ToBase;
-		private final DiffRangeMap baseToP2;
-
-		public MergeStrategy2(List<RangePairSeq> p2EqualToM, DiffRangeMap p1ToBaseRanges, DiffRangeMap baseToP2Ranges) {
-			matches = p2EqualToM;
-			p1ToBase = p1ToBaseRanges;
-			baseToP2= baseToP2Ranges;
-			mergeRanges = new IntSliceSeq(3, 10, 10);
-		}
-
-
-		public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2) {
-			return combineAndMarkRangesWithSource(insPoint, insPoint, start, end, source1, source2);
-		}
-
-		public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2) {
-			mergeRanges.clear();
-			IntSliceSeq mergedLines = new IntSliceSeq(2, end2-start2, 0);
-			for (int i = start2; i < end2; i++) {
-				mergedLines.add(source1, 0);
-			}
-			// [s1Start..s1End) // range in p1 seen as changed in m
-			for (RangePair p1_b : p1ToBase.findInSource(start1, end1)) {
-				// there might be few ranges in (p1-base) that overlap with (p1-m) changes
-				for (RangePair b_p2 : baseToP2.findInSource(p1_b.start2(), p1_b.end2())) {
-					// regions in p2 that correspond to affected regions in base
-					for (int p2Line = b_p2.start2(); p2Line < b_p2.end2(); p2Line++) {
-						for (RangePairSeq eq : matches) {
-							if (eq.includesOriginLine(p2Line)) {
-								// this line in p2 is equal to some line in merge
-								int mergeLine = eq.mapLineIndex(p2Line);
-								if (mergeLine >= start2 && mergeLine < end2) {
-									mergedLines.set(mergeLine - start2, source2, p2Line);
-								}
-							}
-						}
-					}
-				}
-			}
-			int lineCount = 0, start = start2;
-			int lastSeenSource = source1;
-			for (IntTuple t : mergedLines) {
-				if (t.at(0) == lastSeenSource) {
-					lineCount++;
-				} else {
-					if (lineCount > 0) {
-						mergeRanges.add(lastSeenSource, start, lineCount);
-						start += lineCount;
-					}
-					lineCount = 1;
-					lastSeenSource = t.at(0);
-				}
-			}
-			if (lineCount > 0) {
-				mergeRanges.add(lastSeenSource, start, lineCount);
-			}
-			return mergeRanges;
-		}
-
-		public int getLineInP2(int mergeLine) {
-			for (RangePairSeq eq : matches) {
-				if (eq.includesTargetLine(mergeLine)) {
-					return eq.reverseMapLine(mergeLine);
-				}
-			}
-			return -1;
-		}
-	}
-
-
-	private static class AnnotateRev implements RevisionDescriptor {
-		public ContentBlock origin, target;
-		public int originCset, targetCset, mergeCset, fileRevIndex;
-		public HgDataFile df;
-		
-		public void set(HgDataFile file, int fileRev) {
-			df = file;
-			fileRevIndex = fileRev;
-		}
-		public void set(ContentBlock o, ContentBlock t) {
-			origin = o;
-			target = t;
-		}
-		public void set(int o, int t, int m) {
-			originCset = o;
-			targetCset = t;
-			mergeCset = m;
-		}
-		
-		public BlockData origin() {
-			return origin;
-		}
-
-		public BlockData target() {
-			return target;
-		}
-
-		public int originChangesetIndex() {
-			return originCset;
-		}
-
-		public int targetChangesetIndex() {
-			return targetCset;
-		}
-
-		public boolean isMerge() {
-			return mergeCset != NO_REVISION;
-		}
-
-		public int mergeChangesetIndex() {
-			return mergeCset;
-		}
-
-		public int fileRevisionIndex() {
-			return fileRevIndex;
-		}
-		public HgDataFile file() {
-			return df;
-		}
-		@Override
-		public String toString() {
-			if (isMerge()) {
-				return String.format("[%d,%d->%d]", originCset, mergeCset, targetCset);
-			}
-			return String.format("[%d->%d]", originCset, targetCset);
-		}
-	}
-
-	public static void main(String[] args) {
-		EqualBlocksCollector bc = new EqualBlocksCollector();
-		bc.match(-1, 5, 3);
-		bc.match(-1, 10, 2);
-		bc.match(-1, 15, 3);
-		bc.match(-1, 20, 3);
-		IntVector r = new IntVector();
-		bc.intersectWithTarget(7, 10, r);
-		for (int i = 0; i < r.size(); i+=2) {
-			System.out.printf("[%d..%d) ", r.get(i), r.get(i) + r.get(i+1));
-		}
-		System.out.println();
-		MergeStrategy1 ms = new MergeStrategy1(bc.matches);
-		IntSliceSeq mr = ms.doCombine(0, 16, 508, 514);
-		for (IntTuple t : mr) {
-			System.out.printf("%d:[%d..%d)  ", t.at(0), t.at(1), t.at(1) + t.at(2));
-		}
-		System.out.println();
-		System.out.println();
-		DiffRangeMap m1 = new DiffRangeMap(); // p1 -> base
-		m1.match(0, 0, 1); // =1..1 -> 1..1
-		m1.match(7, 3, 0);  // *2..7 -> 2..3
-		DiffRangeMap m2 = new DiffRangeMap(); // base -> p2
-		m2.match(0, 0, 1); // =1..1 -> 1..1
-		m2.match(3, 3, 0); // *2..3 -> 2..3
-		RangePairSeq eq1 = new RangePairSeq();
-		eq1.add(0, 0, 3);
-		RangePairSeq eq2 = new RangePairSeq();
-		eq2.add(0, 4, 3);
-		MergeStrategy2 ms2 = new MergeStrategy2(Arrays.asList(eq1, eq2), m1, m2);
-		mr = ms2.combineAndMarkRangesWithSource(5, 7, 5, 7, 33, 44);
-		for (IntTuple t : mr) {
-			System.out.printf("%d:[%d..%d)  ", t.at(0), t.at(1), t.at(1) + t.at(2));
-		}
-	}
-}
--- a/src/org/tmatesoft/hg/internal/DiffHelper.java	Thu Aug 08 21:32:22 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,437 +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 java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Mercurial cares about changes only up to the line level, e.g. a simple file version dump in manifest looks like (RevlogDump output):
- * 
- *   522:        233748      0        103      17438        433        522      521       -1     756073cf2321df44d3ed0585f2a5754bc8a1b2f6
- *   <PATCH>:
- *   3487..3578, 91:src/org/tmatesoft/hg/core/HgIterateDirection.java\00add61a8a665c5d8f092210767f812fe0d335ac8
- *   
- * I.e. for the {fname}{revision} entry format of manifest, not only {revision} is changed, but the whole line, with unchanged {fname} is recorded
- * in the patch.
- * 
- * Mercurial paper describes reasons for choosing this approach to delta generation, too.
- * 
- * 
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-public class DiffHelper<T extends DiffHelper.ChunkSequence<?>> {
-
-	private Map<Object, IntVector> chunk2UseIndex;
-	private T seq1, seq2;
-
-	// get filled by #longestMatch, track start of common sequence in seq1 and seq2, respectively
-	private int matchStartS1, matchStartS2;
-
-	private MatchInspector<T> matchInspector; 
-
-	public void init(T s1, T s2) {
-		seq1 = s1;
-		seq2 = s2;
-		prepare(s2);
-	}
-	
-	public void init(T s1) {
-		if (seq2 == null) {
-			throw new IllegalStateException("Use this #init() only when target sequence shall be matched against different origin");
-		}
-		seq1 = s1;
-	}
-
-
-	private void prepare(T s2) {
-		chunk2UseIndex = new HashMap<Object, IntVector>();
-		for (int i = 0, len = s2.chunkCount(); i < len; i++) {
-			Object bc = s2.chunk(i);
-			IntVector loc = chunk2UseIndex.get(bc);
-			if (loc == null) {
-				chunk2UseIndex.put(bc, loc = new IntVector());
-			}
-			loc.add(i);
-			// bc.registerUseIn(i) - BEWARE, use of bc here is incorrect
-			// in this case need to find the only ByteChain to keep indexes
-			// i.e. when there are few equal ByteChain instances, notion of "usedIn" shall be either shared (reference same vector)
-			// or kept within only one of them
-		}
-	}
-	
-	public void findMatchingBlocks(MatchInspector<T> insp) {
-		insp.begin(seq1, seq2);
-		matchInspector = insp;
-		findMatchingBlocks(0, seq1.chunkCount(), 0, seq2.chunkCount());
-		insp.end();
-	}
-	
-	/** 
-	 * look up every line in s2 that match lines in s1
-	 * idea: pure additions in s2 are diff-ed against s1 again and again, to see if there are any matches
-	 */
-	void findAllMatchAlternatives(final MatchInspector<T> insp) {
-		assert seq1.chunkCount() > 0;
-		final IntSliceSeq insertions = new IntSliceSeq(2);
-		final boolean matchedAny[] = new boolean[] {false};
-		DeltaInspector<T> myInsp = new DeltaInspector<T>() {
-			@Override
-			protected void unchanged(int s1From, int s2From, int length) {
-				matchedAny[0] = true;
-				insp.match(s1From, s2From, length);
-			}
-			@Override
-			protected void added(int s1InsertPoint, int s2From, int s2To) {
-				insertions.add(s2From, s2To);
-			}
-		};
-		matchInspector = myInsp;
-		myInsp.begin(seq1, seq2);
-		IntSliceSeq s2RangesToCheck = new IntSliceSeq(2, 1, 0);
-		s2RangesToCheck.add(0, seq2.chunkCount());
-		do {
-			IntSliceSeq nextCheck = new IntSliceSeq(2);
-			for (IntTuple t : s2RangesToCheck) {
-				int s2Start = t.at(0);
-				int s2End = t.at(1);
-				myInsp.changeStartS1 = 0;
-				myInsp.changeStartS2 = s2Start;
-				insp.begin(seq1, seq2);
-				matchedAny[0] = false;
-				findMatchingBlocks(0, seq1.chunkCount(), s2Start, s2End);
-				insp.end();
-				myInsp.end();
-				if (matchedAny[0]) {
-					nextCheck.addAll(insertions);
-				}
-				insertions.clear();
-			}
-			s2RangesToCheck = nextCheck;
-		} while (s2RangesToCheck.size() > 0);
-	}
-	
-	/**
-	 * implementation based on Python's difflib.py and SequenceMatcher 
-	 */
-	public int longestMatch(int startS1, int endS1, int startS2, int endS2) {
-		matchStartS1 = matchStartS2 = 0;
-		int maxLength = 0;
-		IntMap<Integer> chunkIndex2MatchCount = new IntMap<Integer>(8);
-		for (int i = startS1; i < endS1; i++) {
-			Object bc = seq1.chunk(i);
-			IntVector occurencesInS2 = chunk2UseIndex.get(bc);
-			if (occurencesInS2 == null) {
-				chunkIndex2MatchCount.clear();
-				continue;
-			}
-			IntMap<Integer> newChunkIndex2MatchCount = new IntMap<Integer>(8);
-			for (int j : occurencesInS2.toArray()) {
-				// s1[i] == s2[j]
-				if (j < startS2) {
-					continue;
-				}
-				if (j >= endS2) {
-					break;
-				}
-				int prevChunkMatches = chunkIndex2MatchCount.containsKey(j-1) ? chunkIndex2MatchCount.get(j-1) : 0;
-				int k = prevChunkMatches + 1;
-				newChunkIndex2MatchCount.put(j, k);
-				if (k > maxLength) {
-					matchStartS1 = i-k+1;
-					matchStartS2 = j-k+1;
-					maxLength = k;
-				}
-			}
-			chunkIndex2MatchCount = newChunkIndex2MatchCount;
-		}
-		return maxLength;
-	}
-	
-	private void findMatchingBlocks(int startS1, int endS1, int startS2, int endS2) {
-		int matchLength = longestMatch(startS1, endS1, startS2, endS2);
-		if (matchLength > 0) {
-			final int saveStartS1 = matchStartS1, saveStartS2 = matchStartS2;
-			if (startS1 < matchStartS1 && startS2 < matchStartS2) {
-				findMatchingBlocks(startS1, matchStartS1, startS2, matchStartS2);
-			}
-			matchInspector.match(saveStartS1, saveStartS2, matchLength);
-			if (saveStartS1+matchLength < endS1 && saveStartS2+matchLength < endS2) {
-				findMatchingBlocks(saveStartS1 + matchLength, endS1, saveStartS2 + matchLength, endS2);
-			}
-		}
-	}
-	
-	public interface MatchInspector<T extends ChunkSequence<?>> {
-		void begin(T s1, T s2);
-		void match(int startSeq1, int startSeq2, int matchLength);
-		void end();
-	}
-	
-	static class MatchDumpInspector<T extends ChunkSequence<?>> implements MatchInspector<T> {
-		private int matchCount;
-
-		public void begin(T s1, T s2) {
-			matchCount = 0;
-		}
-
-		public void match(int startSeq1, int startSeq2, int matchLength) {
-			matchCount++;
-			System.out.printf("match #%d: from line #%d  and line #%d of length %d\n", matchCount, startSeq1, startSeq2, matchLength);
-		}
-
-		public void end() {
-			if (matchCount == 0) {
-				System.out.println("NO MATCHES FOUND!");
-			}
-		}
-	}
-	
-	/**
-	 * Matcher implementation that translates "match/equal" notification to a delta-style "added/removed/changed". 
-	 */
-	public static class DeltaInspector<T extends ChunkSequence<?>> implements MatchInspector<T> {
-		protected int changeStartS1, changeStartS2;
-		protected T seq1, seq2;
-
-		public void begin(T s1, T s2) {
-			seq1 = s1;
-			seq2 = s2;
-			changeStartS1 = changeStartS2 = 0;
-		}
-
-		public void match(int startSeq1, int startSeq2, int matchLength) {
-			reportDeltaElement(startSeq1, startSeq2, matchLength);
-			changeStartS1 = startSeq1 + matchLength;
-			changeStartS2 = startSeq2 + matchLength;
-		}
-
-		public void end() {
-			if (changeStartS1 < seq1.chunkCount()-1 || changeStartS2 < seq2.chunkCount()-1) {
-				reportDeltaElement(seq1.chunkCount()-1, seq2.chunkCount()-1, 0);
-			}
-		}
-
-		protected void reportDeltaElement(int matchStartSeq1, int matchStartSeq2, int matchLength) {
-			if (changeStartS1 < matchStartSeq1) {
-				if (changeStartS2 < matchStartSeq2) {
-					changed(changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2);
-				} else {
-					assert changeStartS2 == matchStartSeq2;
-					deleted(matchStartSeq2, changeStartS1, matchStartSeq1);
-				}
-			} else {
-				assert changeStartS1 == matchStartSeq1;
-				if(changeStartS2 < matchStartSeq2) {
-					added(changeStartS1, changeStartS2, matchStartSeq2);
-				} else {
-					assert changeStartS2 == matchStartSeq2;
-					if (matchStartSeq1 > 0 || matchStartSeq2 > 0) {
-						assert false : String.format("adjustent equal blocks %d, %d and %d,%d", changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2);
-					}
-				}
-			}
-			if (matchLength > 0) {
-				unchanged(matchStartSeq1, matchStartSeq2, matchLength);
-			}
-		}
-
-		/**
-		 * [s1From..s1To) replaced with [s2From..s2To)
-		 */
-		protected void changed(int s1From, int s1To, int s2From, int s2To) {
-			// NO-OP
-		}
-
-		protected void deleted(int s2DeletePoint, int s1From, int s1To) {
-			// NO-OP
-		}
-
-		protected void added(int s1InsertPoint, int s2From, int s2To) {
-			// NO-OP
-		}
-
-		protected void unchanged(int s1From, int s2From, int length) {
-			// NO-OP
-		}
-	}
-	
-	public static class DeltaDumpInspector<T extends ChunkSequence<?>> extends DeltaInspector<T> {
-
-		@Override
-		protected void changed(int s1From, int s1To, int s2From, int s2To) {
-			System.out.printf("changed [%d..%d) with [%d..%d)\n", s1From, s1To, s2From, s2To);
-		}
-		
-		@Override
-		protected void deleted(int s2DeletionPoint, int s1From, int s1To) {
-			System.out.printf("deleted [%d..%d)\n", s1From, s1To);
-		}
-		
-		@Override
-		protected void added(int s1InsertPoint, int s2From, int s2To) {
-			System.out.printf("added [%d..%d) at %d\n", s2From, s2To, s1InsertPoint);
-		}
-
-		@Override
-		protected void unchanged(int s1From, int s2From, int length) {
-			System.out.printf("same [%d..%d) and [%d..%d)\n", s1From, s1From + length, s2From, s2From + length);
-		}
-	}
-	
-	/**
-	 * Generic sequence of chunk, where chunk is anything comparable to another chunk, e.g. a string or a single char
-	 * Sequence diff algorithm above doesn't care about sequence nature.
-	 */
-	public interface ChunkSequence<T> {
-		public T chunk(int index);
-		public int chunkCount();
-	}
-	
-	public static final class LineSequence implements ChunkSequence<LineSequence.ByteChain> {
-		
-		private final byte[] input;
-		private ArrayList<ByteChain> lines;
-
-		public LineSequence(byte[] data) {
-			input = data;
-		}
-		
-		public static LineSequence newlines(byte[] array) {
-			return new LineSequence(array).splitByNewlines();
-		}
-
-		// sequence ends with fake, empty line chunk
-		public LineSequence splitByNewlines() {
-			lines = new ArrayList<ByteChain>();
-			int lastStart = 0;
-			for (int i = 0; i < input.length; i++) {
-				if (input[i] == '\n') {
-					lines.add(new ByteChain(lastStart, i+1));
-					lastStart = i+1;
-				} else if (input[i] == '\r') {
-					if (i+1 < input.length && input[i+1] == '\n') {
-						i++;
-					}
-					lines.add(new ByteChain(lastStart, i+1));
-					lastStart = i+1;
-				}
-			}
-			if (lastStart < input.length) {
-				lines.add(new ByteChain(lastStart, input.length));
-			}
-			// empty chunk to keep offset of input end
-			lines.add(new ByteChain(input.length));
-			return this;
-		}
-		
-		public ByteChain chunk(int index) {
-			return lines.get(index);
-		}
-		
-		public int chunkCount() {
-			return lines.size();
-		}
-		
-		public byte[] data(int chunkFrom, int chunkTo) {
-			if (chunkFrom == chunkTo) {
-				return new byte[0];
-			}
-			int from = chunk(chunkFrom).getOffset(), to = chunk(chunkTo).getOffset();
-			byte[] rv = new byte[to - from];
-			System.arraycopy(input, from, rv, 0, rv.length);
-			return rv;
-		}
-
-		
-		public final class ByteChain {
-			private final int start, end;
-			private final int hash;
-			
-			/**
-			 * construct a chunk with a sole purpose to keep 
-			 * offset of the data end
-			 */
-			ByteChain(int offset) {
-				start = end = offset;
-				// ensure this chunk doesn't match trailing chunk of another sequence
-				hash = System.identityHashCode(this);
-			}
-			
-			ByteChain(int s, int e) {
-				start = s;
-				end = e;
-				hash = calcHash(input, s, e);
-			}
-			
-			/**
-			 * byte offset of the this ByteChain inside ChainSequence 
-			 */
-			public int getOffset() {
-				return start;
-			}
-			
-			public byte[] data() {
-				byte[] rv = new byte[end - start];
-				System.arraycopy(input, start, rv, 0, rv.length);
-				return rv;
-			}
-			
-			@Override
-			public boolean equals(Object obj) {
-				if (obj == null || obj.getClass() != ByteChain.class) {
-					return false;
-				}
-				ByteChain other = (ByteChain) obj;
-				if (other.hash != hash || other.end - other.start != end - start) {
-					return false;
-				}
-				return other.match(input, start);
-			}
-			
-			private boolean match(byte[] oi, int from) {
-				for (int i = start, j = from; i < end; i++, j++) {
-					if (LineSequence.this.input[i] != oi[j]) {
-						return false;
-					}
-				}
-				return true;
-			}
-			
-			@Override
-			public int hashCode() {
-				return hash;
-			}
-			
-			@Override
-			public String toString() {
-				return String.format("[@%d\"%s\"]", start, new String(data()));
-			}
-		}
-
-		// same as Arrays.hashCode(byte[]), just for a slice of a bigger array
-		static int calcHash(byte[] data, int from, int to) {
-			int result = 1;
-			for (int i = from; i < to; i++) {
-				result = 31 * result + data[i];
-			}
-			return result;
-		}
-	}
-}
--- a/src/org/tmatesoft/hg/internal/ForwardAnnotateInspector.java	Thu Aug 08 21:32:22 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +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.HgAnnotateCommand.Inspector;
-import org.tmatesoft.hg.core.HgBlameInspector;
-import org.tmatesoft.hg.core.HgCallbackTargetException;
-import org.tmatesoft.hg.core.HgIterateDirection;
-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 (with actual file change) 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;
-		if (!all.containsKey(revision)) {
-			throw new IllegalArgumentException(String.format("Revision %d has not been visited", revision));
-		}
-		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++, lineInRev+1, 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 blockStart, 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 > blockStart) {
-				// part of origin block from blockStart up to originBlockEnd, but not more
-				// than size of the block (when blockStart is out of block start, i.e. < originPos)
-				int originBlockOverlap = Math.min(originBlockLen, originBlockEnd - blockStart);
-				assert originBlockOverlap > 0;
-				// eat as much as there's left in the block being copied
-				int originBlockConsumed = Math.min(originBlockOverlap, targetBlockLen);
-				int originBlockLine = t.at(2);
-				if (originPos < blockStart) {
-					originBlockLine += originBlockLen-originBlockOverlap;
-				}
-				// copy fragment of original block;
-				current.add(originBlockConsumed, t.at(1), originBlockLine);
-				targetBlockLen -= originBlockConsumed;
-				if (targetBlockLen == 0) {
-					break;
-				}
-			}
-			originPos += originBlockLen;
-		}
-	}
-}
\ No newline at end of file
--- a/src/org/tmatesoft/hg/internal/GeneratePatchInspector.java	Thu Aug 08 21:32:22 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/GeneratePatchInspector.java	Wed Aug 14 14:51:51 2013 +0200
@@ -16,8 +16,9 @@
  */
 package org.tmatesoft.hg.internal;
 
-import org.tmatesoft.hg.internal.DiffHelper.DeltaInspector;
-import org.tmatesoft.hg.internal.DiffHelper.LineSequence;
+import org.tmatesoft.hg.internal.diff.DiffHelper;
+import org.tmatesoft.hg.internal.diff.DiffHelper.DeltaInspector;
+import org.tmatesoft.hg.internal.diff.DiffHelper.LineSequence;
 
 class GeneratePatchInspector extends DeltaInspector<LineSequence> {
 	private final Patch deltaCollector;
--- a/src/org/tmatesoft/hg/internal/LineImpl.java	Thu Aug 08 21:32:22 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +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.HgAnnotateCommand.LineInfo;
-
-/**
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-final class LineImpl implements LineInfo {
-	private int ln;
-	private int origLine;
-	private int rev;
-	private byte[] content;
-
-	void init(int line, int firstAppearance, int csetRev, byte[] cnt) {
-		ln = line;
-		origLine = firstAppearance;
-		rev = csetRev;
-		content = cnt;
-	}
-
-	public int getLineNumber() {
-		return ln;
-	}
-
-
-	public int getOriginLineNumber() {
-		return origLine;
-	}
-
-	public int getChangesetIndex() {
-		return rev;
-	}
-
-	public byte[] getContent() {
-		return content;
-	}
-}
\ No newline at end of file
--- a/src/org/tmatesoft/hg/internal/RangePairSeq.java	Thu Aug 08 21:32:22 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +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 java.util.Formatter;
-
-/**
- * Sequence of range pairs (denoted origin and target), {originStart, targetStart, length}, tailored for diff/annotate
- * 
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-public final class RangePairSeq {
-	private final IntSliceSeq ranges = new IntSliceSeq(3);
-	
-	public void add(int start1, int start2, int length) {
-		int count = ranges.size();
-		if (count > 0) {
-			int lastS1 = ranges.get(--count, 0);
-			int lastS2 = ranges.get(count, 1);
-			int lastLen = ranges.get(count, 2);
-			if (start1 == lastS1 + lastLen && start2 == lastS2 + lastLen) {
-				// new range continues the previous one - just increase the length
-				ranges.set(count, lastS1, lastS2, lastLen + length);
-				return;
-			}
-		}
-		ranges.add(start1, start2, length);
-	}
-	
-	public void clear() {
-		ranges.clear();
-	}
-
-	public int size() {
-		return ranges.size();
-	}
-
-	/**
-	 * find out line index in the target that matches specified origin line
-	 */
-	public int mapLineIndex(int ln) {
-		for (IntTuple t : ranges) {
-			int s1 = t.at(0);
-			if (s1 > ln) {
-				return -1;
-			}
-			int l = t.at(2);
-			if (s1 + l > ln) {
-				int s2 = t.at(1);
-				return s2 + (ln - s1);
-			}
-		}
-		return -1;
-	}
-	
-	/**
-	 * find out line index in origin that matches specified target line
-	 */
-	public int reverseMapLine(int targetLine) {
-		for (IntTuple t : ranges) {
-			int ts = t.at(1);
-			if (ts > targetLine) {
-				return -1;
-			}
-			int l = t.at(2);
-			if (ts + l > targetLine) {
-				int os = t.at(0);
-				return os + (targetLine - ts);
-			}
-		}
-		return -1;
-	}
-	
-	public RangePairSeq intersect(RangePairSeq target) {
-		RangePairSeq v = new RangePairSeq();
-		for (IntTuple t : ranges) {
-			int originLine = t.at(0);
-			int targetLine = t.at(1);
-			int length = t.at(2);
-			int startTargetLine = -1, startOriginLine = -1, c = 0;
-			for (int j = 0; j < length; j++) {
-				int lnInFinal = target.mapLineIndex(targetLine + j);
-				if (lnInFinal == -1 || (startTargetLine != -1 && lnInFinal != startTargetLine + c)) {
-					// the line is not among "same" in ultimate origin
-					// or belongs to another/next "same" chunk 
-					if (startOriginLine == -1) {
-						continue;
-					}
-					v.add(startOriginLine, startTargetLine, c);
-					c = 0;
-					startOriginLine = startTargetLine = -1;
-					// fall-through to check if it's not complete miss but a next chunk
-				}
-				if (lnInFinal != -1) {
-					if (startOriginLine == -1) {
-						startOriginLine = originLine + j;
-						startTargetLine = lnInFinal;
-						c = 1;
-					} else {
-						// lnInFinal != startTargetLine + s is covered above
-						assert lnInFinal == startTargetLine + c;
-						c++;
-					}
-				}
-			}
-			if (startOriginLine != -1) {
-				assert c > 0;
-				v.add(startOriginLine, startTargetLine, c);
-			}
-		}
-		return v;
-	}
-	
-	// true when specified line in origin is equal to a line in target
-	public boolean includesOriginLine(int ln) {
-		return includes(ln, 0);
-	}
-	
-	// true when specified line in target is equal to a line in origin
-	public boolean includesTargetLine(int ln) {
-		return includes(ln, 1);
-	}
-
-	private boolean includes(int ln, int o) {
-		for (IntTuple t : ranges) {
-			int rangeStart = t.at(o);
-			if (rangeStart > ln) {
-				return false;
-			}
-			int rangeLen = t.at(2);
-			if (rangeStart + rangeLen > ln) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	public CharSequence dump() {
-		StringBuilder sb = new StringBuilder();
-		Formatter f = new Formatter(sb);
-		for (IntTuple t : ranges) {
-			int s1 = t.at(0);
-			int s2 = t.at(1);
-			int len = t.at(2);
-			f.format("[%d..%d) == [%d..%d);  ", s1, s1 + len, s2, s2 + len);
-		}
-		return sb;
-	}
-	
-	@Override
-	public String toString() {
-		return String.format("RangeSeq[%d]:%s", size(), dump());
-	}
-}
\ No newline at end of file
--- a/src/org/tmatesoft/hg/internal/ReverseAnnotateInspector.java	Thu Aug 08 21:32:22 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +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 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;
-	private int[] lineNumbers;
-
-	/**
-	 * @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, lineNumbers[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 [knownLines.length];
-			Arrays.fill(lineRevisions, NO_REVISION);
-			lineNumbers = new int[knownLines.length];
-			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, ln, block.targetChangesetIndex());
-				}
-				knownLines[lnInFinal] = true;
-			}
-		}
-	}
-
-	public void changed(ChangeBlock block) {
-		added(block);
-	}
-
-	public void deleted(DeleteBlock block) {
-	}
-
-	private void line(int lineNumber, int firstAppearance, int changesetRevIndex) {
-		lineRevisions[lineNumber] = changesetRevIndex;
-		lineNumbers[lineNumber] = firstAppearance;
-	}
-}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/diff/BlameHelper.java	Wed Aug 14 14:51:51 2013 +0200
@@ -0,0 +1,972 @@
+/*
+ * 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.diff;
+
+import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew;
+import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+import org.tmatesoft.hg.core.HgCallbackTargetException;
+import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.internal.ByteArrayChannel;
+import org.tmatesoft.hg.internal.FileHistory;
+import org.tmatesoft.hg.internal.FileRevisionHistoryChunk;
+import org.tmatesoft.hg.internal.IntSliceSeq;
+import org.tmatesoft.hg.internal.IntTuple;
+import org.tmatesoft.hg.internal.IntVector;
+import org.tmatesoft.hg.internal.diff.DiffHelper.LineSequence;
+import org.tmatesoft.hg.internal.diff.DiffHelper.LineSequence.ByteChain;
+import org.tmatesoft.hg.internal.diff.DiffRangeMap.RangePair;
+import org.tmatesoft.hg.core.HgBlameInspector;
+import org.tmatesoft.hg.core.HgBlameInspector.*;
+import org.tmatesoft.hg.repo.HgChangelog;
+import org.tmatesoft.hg.repo.HgDataFile;
+import org.tmatesoft.hg.repo.HgInvalidStateException;
+import org.tmatesoft.hg.repo.HgParentChildMap;
+import org.tmatesoft.hg.repo.HgRepository;
+import org.tmatesoft.hg.repo.HgRevisionMap;
+import org.tmatesoft.hg.repo.HgRuntimeException;
+import org.tmatesoft.hg.util.Adaptable;
+import org.tmatesoft.hg.util.CancelledException;
+import org.tmatesoft.hg.util.Pair;
+
+/**
+ * Blame implementation
+ * @see HgBlameInspector
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class BlameHelper {
+	
+	private final HgBlameInspector insp;
+	private FileLinesCache linesCache;
+	private HgParentChildMap<HgChangelog> clogMap;
+
+	public BlameHelper(HgBlameInspector inspector) {
+		insp = inspector;
+	}
+
+	/**
+	 * Build history of the file for the specified range (follow renames if necessary). This history
+	 * is used to access various file revision data during subsequent {@link #diff(int, int, int, int)} and
+	 * {@link #annotateChange(int, int, int[], int[])} calls. Callers can use returned history for own approaches 
+	 * to iteration over file history.
+
+	 * <p>NOTE, clogRevIndexEnd has to list name of the supplied file in the corresponding manifest,
+	 * as it's not possible to trace rename history otherwise.
+	 */
+	public FileHistory prepare(HgDataFile df, int clogRevIndexStart, int clogRevIndexEnd) throws HgRuntimeException {
+		assert clogRevIndexStart <= clogRevIndexEnd;
+		FileHistory fileHistory = new FileHistory(df, clogRevIndexStart, clogRevIndexEnd);
+		fileHistory.build();
+		int cacheHint = 5; // cache comes useful when we follow merge branches and don't want to
+		// parse base revision twice. There's no easy way to determine max(distance(all(base,merge))),
+		// hence the heuristics to use the longest history chunk:
+		for (FileRevisionHistoryChunk c : fileHistory.iterate(OldToNew)) {
+			// iteration order is not important here
+			if (c.revisionCount() > cacheHint) {
+				cacheHint = c.revisionCount();
+			}
+		}
+		linesCache = new FileLinesCache(cacheHint);
+		for (FileRevisionHistoryChunk fhc : fileHistory.iterate(OldToNew)) {
+			// iteration order is not important here
+			linesCache.useFileUpTo(fhc.getFile(), fhc.getEndChangeset());
+		}
+		return fileHistory;
+	}
+	
+	// NO_REVISION is not allowed as any argument
+	public void diff(int fileRevIndex1, int clogRevIndex1, int fileRevIndex2, int clogRevIndex2) throws HgCallbackTargetException, HgRuntimeException {
+		HgDataFile targetFile = linesCache.getFile(clogRevIndex2);
+		LineSequence c1 = linesCache.lines(clogRevIndex1, fileRevIndex1);
+		LineSequence c2 = linesCache.lines(clogRevIndex2, fileRevIndex2);
+		DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>();
+		pg.init(c1, c2);
+		BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex2, insp, clogRevIndex1, clogRevIndex2);
+		pg.findMatchingBlocks(bbi);
+		bbi.checkErrors();
+	}
+
+	public void annotateChange(int fileRevIndex, int csetRevIndex, int[] fileParentRevs, int[] fileParentClogRevs) throws HgCallbackTargetException, HgRuntimeException {
+		HgDataFile targetFile = linesCache.getFile(csetRevIndex);
+		final LineSequence fileRevLines = linesCache.lines(csetRevIndex, fileRevIndex);
+		if (fileParentClogRevs[0] != NO_REVISION && fileParentClogRevs[1] != NO_REVISION) {
+			int p1ClogIndex = fileParentClogRevs[0];
+			int p2ClogIndex = fileParentClogRevs[1];
+			LineSequence p1Lines = linesCache.lines(p1ClogIndex, fileParentRevs[0]);
+			LineSequence p2Lines = linesCache.lines(p2ClogIndex, fileParentRevs[1]);
+			MergeResolutionStrategy mergeResolver = createMergeStrategy(fileRevLines, p1Lines, p2Lines, csetRevIndex, fileParentClogRevs);
+			//
+			DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>();
+			pg.init(p1Lines, fileRevLines);
+			BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex, insp, p1ClogIndex, csetRevIndex);
+			bbi.setMergeParent2(mergeResolver, p2ClogIndex);
+			pg.findMatchingBlocks(bbi);
+			bbi.checkErrors();
+		} else if (fileParentClogRevs[0] == fileParentClogRevs[1]) {
+			// may be equal iff both are unset
+			assert fileParentClogRevs[0] == NO_REVISION;
+			// everything added
+			BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex, insp, NO_REVISION, csetRevIndex);
+			bbi.begin(LineSequence.newlines(new byte[0]), fileRevLines);
+			bbi.match(0, fileRevLines.chunkCount()-1, 0);
+			bbi.end();
+			bbi.checkErrors();
+		} else {
+			int soleParentIndex = fileParentClogRevs[0] == NO_REVISION ? 1 : 0;
+			assert fileParentClogRevs[soleParentIndex] != NO_REVISION;
+			LineSequence parentLines = linesCache.lines(fileParentClogRevs[soleParentIndex], fileParentRevs[soleParentIndex]);
+			
+			DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>();
+			pg.init(parentLines, fileRevLines);
+			BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex, insp, fileParentClogRevs[soleParentIndex], csetRevIndex);
+			pg.findMatchingBlocks(bbi);
+			bbi.checkErrors();
+		}
+	}
+	
+	private static final boolean useNewStrategy = Boolean.TRUE.booleanValue();
+	
+	private MergeResolutionStrategy createMergeStrategy(LineSequence fileRevLines, LineSequence p1Lines, LineSequence p2Lines, int csetRevIndex, int[] fileParentClogRevs) {
+		DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>();
+		if (useNewStrategy) {
+			final ArrayList<RangePairSeq> allMatches = new ArrayList<RangePairSeq>();
+			pg.init(p2Lines, fileRevLines);
+			pg.findAllMatchAlternatives(new DiffHelper.MatchInspector<LineSequence>() {
+				private RangePairSeq matches;
+
+				public void begin(LineSequence s1, LineSequence s2) {
+					matches = new RangePairSeq();
+				}
+
+				public void match(int startSeq1, int startSeq2, int matchLength) {
+					matches.add(startSeq1, startSeq2, matchLength);
+				}
+
+				public void end() {
+					if (matches.size() > 0) {
+						allMatches.add(matches);
+					}
+				}
+				
+			});
+			//
+			LineSequence baseLines = getBaseRevisionLines(csetRevIndex, fileParentClogRevs);
+			pg.init(p1Lines, baseLines);
+			DiffRangeMap p1ToBase = new DiffRangeMap().fill(pg);
+			pg.init(baseLines, p2Lines);
+			DiffRangeMap baseToP2 = new DiffRangeMap().fill(pg);
+			return new MergeStrategy2(allMatches, p1ToBase, baseToP2);
+		} else {
+			pg.init(p2Lines, fileRevLines);
+			EqualBlocksCollector p2MergeCommon = new EqualBlocksCollector();
+			pg.findMatchingBlocks(p2MergeCommon);
+			return new MergeStrategy1(p2MergeCommon.matches);
+		}
+	}
+
+	private LineSequence getBaseRevisionLines(int clogRevIndex, int[] fileParentClogRevs) {
+		assert fileParentClogRevs[0] >= 0;
+		assert fileParentClogRevs[1] >= 0;
+		HgDataFile targetFile = linesCache.getFile(clogRevIndex);
+		final HgRepository repo = targetFile.getRepo();
+		if (clogMap == null) {
+			// FIXME replace HgParentChildMap with revlog.indexWalk(AncestorIterator))
+			clogMap = new HgParentChildMap<HgChangelog>(repo.getChangelog());
+			clogMap.init();
+		}
+		final HgRevisionMap<HgChangelog> m = clogMap.getRevisionMap();
+		Nodeid ancestor = clogMap.ancestor(m.revision(fileParentClogRevs[0]), m.revision(fileParentClogRevs[1]));
+		final int ancestorRevIndex = m.revisionIndex(ancestor);
+		Nodeid fr = repo.getManifest().getFileRevision(ancestorRevIndex, targetFile.getPath());
+		if (fr == null) {
+			return LineSequence.newlines(new byte[0]);
+		}
+		return linesCache.lines(ancestorRevIndex, targetFile.getRevisionIndex(fr));
+	}
+
+	private static class FileLinesCache {
+		private final LinkedList<Pair<Integer, LineSequence>> lruCache;
+		private final int limit;
+		private final LinkedList<Pair<Integer, HgDataFile>> files; // TODO in fact, need sparse array 
+
+		/**
+		 * @param lruLimit how many parsed file revisions to keep
+		 */
+		public FileLinesCache(int lruLimit) {
+			limit = lruLimit;
+			lruCache = new LinkedList<Pair<Integer, LineSequence>>();
+			files = new LinkedList<Pair<Integer,HgDataFile>>();
+		}
+		
+		public void useFileUpTo(HgDataFile df, int clogRevIndex) {
+			Pair<Integer, HgDataFile> newEntry = new Pair<Integer, HgDataFile>(clogRevIndex, df);
+			for (ListIterator<Pair<Integer, HgDataFile>> it = files.listIterator(); it.hasNext();) {
+				Pair<Integer, HgDataFile> e = it.next();
+				if (e.first() == clogRevIndex) {
+					assert e.second().getPath().equals(df.getPath());
+					return;
+				}
+				if (e.first() > clogRevIndex) {
+					// insert new entry before current
+					it.previous();
+					it.add(newEntry);
+					return;
+				}
+			}
+			files.add(newEntry);
+		}
+		
+		public HgDataFile getFile(int clogRevIndex) {
+			for (Pair<Integer, HgDataFile> e : files) {
+				if (e.first() >= clogRevIndex) {
+					return e.second();
+				}
+			}
+			throw new HgInvalidStateException(String.format("Got %d file-changelog mappings, but no luck for revision %d.", files.size(), clogRevIndex));
+		}
+
+		public LineSequence lines(int clogRevIndex, int fileRevIndex) throws HgRuntimeException {
+			Pair<Integer, LineSequence> cached = checkCache(clogRevIndex);
+			if (cached != null) {
+				return cached.second();
+			}
+			HgDataFile df = getFile(clogRevIndex);
+			try {
+				ByteArrayChannel c;
+				df.content(fileRevIndex, c = new ByteArrayChannel());
+				LineSequence rv = LineSequence.newlines(c.toArray());
+				lruCache.addFirst(new Pair<Integer, LineSequence>(clogRevIndex, rv));
+				if (lruCache.size() > limit) {
+					lruCache.removeLast();
+				}
+				return rv;
+			} catch (CancelledException ex) {
+				// TODO likely it was bad idea to throw cancelled exception from content()
+				// deprecate and provide alternative?
+				HgInvalidStateException ise = new HgInvalidStateException("ByteArrayChannel never throws CancelledException");
+				ise.initCause(ex);
+				throw ise;
+			}
+		}
+		
+		private Pair<Integer,LineSequence> checkCache(int fileRevIndex) {
+			Pair<Integer, LineSequence> rv = null;
+			for (ListIterator<Pair<Integer, LineSequence>> it = lruCache.listIterator(); it.hasNext(); ) {
+				Pair<Integer, LineSequence> p = it.next();
+				if (p.first() == fileRevIndex) {
+					rv = p;
+					it.remove();
+					break;
+				}
+			}
+			if (rv != null) {
+				lruCache.addFirst(rv);
+			}
+			return rv;
+		}
+	}
+
+	private static class BlameBlockInspector extends DiffHelper.DeltaInspector<LineSequence> {
+		private final HgBlameInspector insp;
+		private final int csetOrigin;
+		private final int csetTarget;
+		private MergeResolutionStrategy p2MergeCommon;
+		private int csetMergeParent;
+		private final AnnotateRev annotatedRevision;
+		private HgCallbackTargetException error;
+
+		public BlameBlockInspector(HgDataFile df, int fileRevIndex, HgBlameInspector inspector, int originCset, int targetCset) {
+			assert inspector != null;
+			insp = inspector;
+			annotatedRevision = new AnnotateRev();
+			annotatedRevision.set(df, fileRevIndex);
+			csetOrigin = originCset;
+			csetTarget = targetCset;
+		}
+		
+		public void setMergeParent2(MergeResolutionStrategy p2MergeStrategy, int parentCset2) {
+			p2MergeCommon = p2MergeStrategy;
+			csetMergeParent = parentCset2;
+		}
+		
+		@Override
+		public void begin(LineSequence s1, LineSequence s2) {
+			super.begin(s1, s2);
+			if (shallStop()) {
+				return;
+			}
+			ContentBlock originContent = new ContentBlock(s1);
+			ContentBlock targetContent = new ContentBlock(s2);
+			annotatedRevision.set(originContent, targetContent);
+			annotatedRevision.set(csetOrigin, csetTarget, p2MergeCommon != null ? csetMergeParent : NO_REVISION);
+			RevisionDescriptor.Recipient curious = Adaptable.Factory.getAdapter(insp, RevisionDescriptor.Recipient.class, null);
+			if (curious != null) {
+				try {
+					curious.start(annotatedRevision);
+				} catch (HgCallbackTargetException ex) {
+					error = ex;
+				}
+			}
+		}
+		
+		@Override
+		public void end() {
+			super.end();
+			if (shallStop()) {
+				return;
+			}
+			RevisionDescriptor.Recipient curious = Adaptable.Factory.getAdapter(insp, RevisionDescriptor.Recipient.class, null);
+			if (curious != null) {
+				try {
+					curious.done(annotatedRevision);
+				} catch (HgCallbackTargetException ex) {
+					error = ex;
+				}
+			}
+			p2MergeCommon = null;
+		}
+
+		@Override
+		protected void changed(int s1From, int s1To, int s2From, int s2To) {
+			if (shallStop()) {
+				return;
+			}
+			try {
+				if (p2MergeCommon != null) {
+					IntSliceSeq mergeRanges = p2MergeCommon.combineAndMarkRangesWithSource(s1From, s1To, s2From, s2To, csetOrigin, csetMergeParent);
+					
+					/*
+					 * Usecases, how it USED TO BE initially:
+					 * 3 lines changed to 10 lines. range of 10 lines breaks down to 2 from p2, 3 from p1, and 5 from p2.
+					 * We report: 2 lines changed to 2(p2), then 1 line changed with 3(p1) and 5 lines added from p2.
+					 * 
+					 * 10 lines changed to 3 lines, range of 3 lines breaks down to 2 line from p1 and 1 line from p2.
+					 * We report: 2 lines changed to 2(p1) and 8 lines changed to 1(p2)
+					 * 
+					 * NOW, lines from p2 are always reported as pure add (since we need their insertion point to be in p2, not in p1)
+					 * and we try to consume p1 changes as soon as we see first p1's range 
+					 */
+					int s1TotalLines = s1To - s1From, s1ConsumedLines = 0, s1Start = s1From;
+					
+					for (Iterator<IntTuple> it = mergeRanges.iterator(); it.hasNext();) {
+						IntTuple mergeRange = it.next();
+						final int rangeOrigin = mergeRange.at(0);
+						final int rangeStart = mergeRange.at(1);
+						final int rangeLen = mergeRange.at(2);
+						final boolean lastRange = it.hasNext();
+						final int s1LinesLeft = s1TotalLines - s1ConsumedLines;
+						// how many lines we may report as changed (don't use more than in range unless it's the very last range)
+						final int s1LinesToBorrow = lastRange ? s1LinesLeft : Math.min(s1LinesLeft, rangeLen);
+						if (rangeOrigin != csetMergeParent && s1LinesToBorrow > 0) {
+							ChangeBlockImpl block = getChangeBlock(s1Start, s1LinesToBorrow, rangeStart, rangeLen);
+							block.setOriginAndTarget(rangeOrigin, csetTarget);
+							insp.changed(block);
+							s1ConsumedLines += s1LinesToBorrow;
+							s1Start += s1LinesToBorrow;
+						} else {
+							int blockInsPoint = rangeOrigin != csetMergeParent ? s1Start : p2MergeCommon.getLineInP2(rangeStart);
+							ChangeBlockImpl block = getAddBlock(rangeStart, rangeLen, blockInsPoint);
+							block.setOriginAndTarget(rangeOrigin, csetTarget);
+							insp.added(block);
+						}
+					}
+					if (s1ConsumedLines != s1TotalLines) {
+						assert s1ConsumedLines < s1TotalLines : String.format("Expected to process %d lines, but actually was %d", s1TotalLines, s1ConsumedLines);
+						// either there were no ranges from p1, whole s2From..s2To range came from p2, shall report as deleted
+						// or the ranges found were not enough to consume whole s2From..s2To
+						// The "deletion point" is shifted to the end of last csetOrigin->csetTarget change
+						int s2DeletePoint = s2From + s1ConsumedLines;
+						ChangeBlockImpl block =  new ChangeBlockImpl(annotatedRevision.origin, null, s1Start, s1To - s1Start, -1, -1, -1, s2DeletePoint);
+						block.setOriginAndTarget(csetOrigin, csetTarget);
+						insp.deleted(block);
+					}
+				} else {
+					ChangeBlockImpl block = getChangeBlock(s1From, s1To - s1From, s2From, s2To - s2From);
+					block.setOriginAndTarget(csetOrigin, csetTarget);
+					insp.changed(block);
+				}
+			} catch (HgCallbackTargetException ex) {
+				error = ex;
+			}
+		}
+		
+		@Override
+		protected void added(int s1InsertPoint, int s2From, int s2To) {
+			if (shallStop()) {
+				return;
+			}
+			try {
+				if (p2MergeCommon != null) {
+					IntSliceSeq mergeRanges = p2MergeCommon.combineAndMarkRangesWithSource(s1InsertPoint, s2From, s2To, csetOrigin, csetMergeParent);
+					int insPoint = s1InsertPoint; // track changes to insertion point
+					for (IntTuple mergeRange : mergeRanges) {
+						int rangeOrigin = mergeRange.at(0);
+						int rangeStart = mergeRange.at(1);
+						int rangeLen = mergeRange.at(2);
+						// XXX likely need somewhat similar to the code above: 
+						// int blockInsPoint = rangeOrigin != csetMergeParent ? s1Start : p2MergeCommon.reverseMapLine(rangeStart);
+						//
+						ChangeBlockImpl block = getAddBlock(rangeStart, rangeLen, insPoint);
+						block.setOriginAndTarget(rangeOrigin, csetTarget);
+						insp.added(block);
+						// indicate insPoint moved down number of lines we just reported
+						insPoint += rangeLen;
+					}
+				} else {
+					ChangeBlockImpl block = getAddBlock(s2From, s2To - s2From, s1InsertPoint);
+					block.setOriginAndTarget(csetOrigin, csetTarget);
+					insp.added(block);
+				}
+			} catch (HgCallbackTargetException ex) {
+				error = ex;
+			}
+		}
+		
+		@Override
+		protected void deleted(int s2DeletePoint, int s1From, int s1To) {
+			if (shallStop()) {
+				return;
+			}
+			try {
+				ChangeBlockImpl block = new ChangeBlockImpl(annotatedRevision.origin, null, s1From, s1To - s1From, -1, -1, -1, s2DeletePoint);
+				block.setOriginAndTarget(csetOrigin, csetTarget);
+				insp.deleted(block);
+			} catch (HgCallbackTargetException ex) {
+				error = ex;
+			}
+		}
+
+		@Override
+		protected void unchanged(int s1From, int s2From, int length) {
+			if (shallStop()) {
+				return;
+			}
+			try {
+				EqualBlockImpl block = new EqualBlockImpl(s1From, s2From, length, annotatedRevision.target);
+				block.setOriginAndTarget(csetOrigin, csetTarget);
+				insp.same(block);
+			} catch (HgCallbackTargetException ex) {
+				error = ex;
+			}
+		}
+		
+		void checkErrors() throws HgCallbackTargetException {
+			if (error != null) {
+				throw error;
+			}
+		}
+		
+		private boolean shallStop() {
+			return error != null;
+		}
+		
+		private ChangeBlockImpl getAddBlock(int start, int len, int insPoint) {
+			return new ChangeBlockImpl(null, annotatedRevision.target, -1, -1, start, len, insPoint, -1);
+		}
+		
+		private ChangeBlockImpl getChangeBlock(int start1, int len1, int start2, int len2) {
+			return new ChangeBlockImpl(annotatedRevision.origin, annotatedRevision.target, start1, len1, start2, len2, start1, start2);
+		}
+	}
+	
+	private static class BlockImpl implements Block {
+		private int originCset;
+		private int targetCset;
+
+		void setOriginAndTarget(int originChangesetIndex, int targetChangesetIndex) {
+			// XXX perhaps, shall be part of Inspector API, rather than Block's
+			// as they don't change between blocks (although the moment about merged revisions)
+			// is not yet clear to me
+			originCset = originChangesetIndex;
+			targetCset = targetChangesetIndex;
+		}
+
+		public int originChangesetIndex() {
+			return originCset;
+		}
+
+		public int targetChangesetIndex() {
+			return targetCset;
+		}
+	}
+
+	private static class EqualBlockImpl extends BlockImpl implements EqualBlock {
+		private final int start1, start2;
+		private final int length;
+		private final ContentBlock fullContent;
+		private FilterBlock myContent;
+		
+		EqualBlockImpl(int blockStartSeq1, int blockStartSeq2, int blockLength, ContentBlock targetContent) {
+			start1 = blockStartSeq1;
+			start2 = blockStartSeq2;
+			length = blockLength;
+			fullContent = targetContent;
+		}
+
+		public int originStart() {
+			return start1;
+		}
+
+		public int targetStart() {
+			return start2;
+		}
+
+		public int length() {
+			return length;
+		}
+		
+		public BlockData content() {
+			if (myContent == null) {
+				myContent = new FilterBlock(fullContent, start2, length);
+			}
+			return myContent;
+		}
+		
+		@Override
+		public String toString() {
+			return String.format("@@ [%d..%d) == [%d..%d) @@", start1, start1+length, start2, start2+length);
+		}
+	}
+	
+	private static class ChangeBlockImpl extends BlockImpl implements ChangeBlock {
+		private final ContentBlock oldContent;
+		private final ContentBlock newContent;
+		private final int s1Start;
+		private final int s1Len;
+		private final int s2Start;
+		private final int s2Len;
+		private final int s1InsertPoint;
+		private final int s2DeletePoint;
+		private FilterBlock addedBlock, removedBlock;
+
+		public ChangeBlockImpl(ContentBlock c1, ContentBlock c2, int s1Start, int s1Len, int s2Start, int s2Len, int s1InsertPoint, int s2DeletePoint) {
+			oldContent = c1;
+			newContent = c2;
+			this.s1Start = s1Start;
+			this.s1Len = s1Len;
+			this.s2Start = s2Start;
+			this.s2Len = s2Len;
+			this.s1InsertPoint = s1InsertPoint;
+			this.s2DeletePoint = s2DeletePoint;
+		}
+		
+		public int insertedAt() {
+			return s1InsertPoint;
+		}
+
+		public int firstAddedLine() {
+			return s2Start;
+		}
+
+		public int totalAddedLines() {
+			return s2Len;
+		}
+
+		public BlockData addedLines() {
+			if (addedBlock == null) {
+				addedBlock = new FilterBlock(newContent, firstAddedLine(), totalAddedLines());
+			}
+			return addedBlock;
+		}
+		
+		public int removedAt() {
+			return s2DeletePoint;
+		}
+
+		public int firstRemovedLine() {
+			return s1Start;
+		}
+
+		public int totalRemovedLines() {
+			return s1Len;
+		}
+
+		public BlockData removedLines() {
+			if (removedBlock == null) {
+				removedBlock = new FilterBlock(oldContent, firstRemovedLine(), totalRemovedLines());
+			}
+			return removedBlock;
+		}
+		
+		@Override
+		public String toString() {
+			if (s2DeletePoint == -1) {
+				return String.format("@@ -%d,0 +%d,%d @@", insertedAt(), firstAddedLine(), totalAddedLines());
+			} else if (s1InsertPoint == -1) {
+				// delete only
+				return String.format("@@ -%d,%d +%d,0 @@", firstRemovedLine(), totalRemovedLines(), removedAt());
+			}
+			return String.format("@@ -%d,%d +%d,%d @@", firstRemovedLine(), totalRemovedLines(), firstAddedLine(), totalAddedLines());
+		}
+	}
+	
+	private static class SingleLine implements BlockData {
+		private final ByteChain line;
+
+		public SingleLine(ByteChain lineContent) {
+			line = lineContent;
+		}
+
+		public BlockData elementAt(int index) {
+			assert false;
+			return null;
+		}
+
+		public int elementCount() {
+			return 0;
+		}
+
+		public byte[] asArray() {
+			return line.data();
+		}
+	}
+	
+	private static class ContentBlock implements BlockData {
+		private final LineSequence seq;
+
+		public ContentBlock(LineSequence sequence) {
+			seq = sequence;
+		}
+
+		public BlockData elementAt(int index) {
+			return new SingleLine(seq.chunk(index));
+		}
+
+		public int elementCount() {
+			return seq.chunkCount() - 1;
+		}
+
+		public byte[] asArray() {
+			return seq.data(0, seq.chunkCount() - 1);
+		}
+	}
+	
+	private static class FilterBlock implements BlockData {
+		private final ContentBlock contentBlock;
+		private final int from;
+		private final int length;
+
+		public FilterBlock(ContentBlock bd, int startFrom, int len) {
+			assert bd != null;
+			assert startFrom + len < bd.seq.chunkCount(); // there's one extra chunk in the end, so strict less is ok
+			contentBlock = bd;
+			from = startFrom;
+			length = len;
+		}
+
+		public BlockData elementAt(int index) {
+			if (index < 0 || index >= length) {
+				throw new IllegalArgumentException(String.format("Expected value from [0..%d), got %d", length, index));
+			}
+			return contentBlock.elementAt(from + index);
+		}
+
+		public int elementCount() {
+			return length;
+		}
+
+		public byte[] asArray() {
+			return contentBlock.seq.data(from, from + length);
+		}
+	}
+
+	private static class EqualBlocksCollector implements DiffHelper.MatchInspector<LineSequence> {
+		private final RangePairSeq matches = new RangePairSeq();
+
+		public void begin(LineSequence s1, LineSequence s2) {
+		}
+
+		public void match(int startSeq1, int startSeq2, int matchLength) {
+			matches.add(startSeq1, startSeq2, matchLength);
+		}
+
+		public void end() {
+		}
+
+		public void intersectWithTarget(int start, int length, IntVector result) {
+			int s = start;
+			for (int l = start, x = start + length; l < x; l++) {
+				if (!matches.includesTargetLine(l)) {
+					if (l - s > 0) {
+						result.add(s);
+						result.add(l - s);
+					}
+					s = l+1;
+				}
+			}
+			if (s < start+length) {
+				result.add(s);
+				result.add((start + length) - s);
+			}
+		}
+		
+	}
+
+	interface MergeResolutionStrategy {
+		/**
+		 * breaks region [start2..end2) into ranges according to deduced (or simply guessed)
+		 * matching of [start1..end1) lines to lines in source1 and source2
+		 * @return list of tuples (source, start, length), where source is one of the identifiers supplied
+		 */
+		public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2);
+		public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2);
+		public int getLineInP2(int mergeLine);
+	}
+
+	// report lines as merged from p2 solely based on whether target line belongs
+	// to a region that is equal to p2 region
+	private static class MergeStrategy1 implements MergeResolutionStrategy {
+		// equal ranges in p2 and merged revision
+		private final RangePairSeq matches; 
+		private final IntSliceSeq mergeRanges;
+
+		public MergeStrategy1(RangePairSeq p2EqualToM) {
+			matches = p2EqualToM;
+			mergeRanges = new IntSliceSeq(3, 10, 10);
+		}
+
+		/*
+		 * intersects [start..start+length) with ranges of target lines, and based on the intersection 
+		 * breaks initial range into smaller ranges and records them into result, with marker to indicate
+		 * whether the range is from initial range (markerSource) or is a result of the intersection with target
+		 * (markerTarget)
+		 */
+		private IntSliceSeq doCombine(int start, int length, int markerSource, int markerTarget) {
+			mergeRanges.clear();
+			assert mergeRanges.sliceSize() == 3;
+			int sourceStart = start, targetStart = start, sourceEnd = start + length;
+			for (int l = sourceStart; l < sourceEnd; l++) {
+				if (matches.includesTargetLine(l)) {
+					// l is from target
+					if (sourceStart < l) {
+						// few lines from source range were not in the target, report them
+						mergeRanges.add(markerSource, sourceStart, l - sourceStart);
+					}
+					// indicate the earliest line from source range to use
+					sourceStart = l + 1;
+				} else {
+					// l is not in target
+					if (targetStart < l) {
+						// report lines from target range
+						mergeRanges.add(markerTarget, targetStart, l - targetStart);
+					}
+					// next line *may* be from target
+					targetStart = l + 1;
+				}
+			}
+			// if source range end with line from target, sourceStart would be == sourceEnd, and we need to add range with markerTarget
+			// if source range doesn't end with target line, targetStart == sourceEnd, while sourceStart < sourceEnd
+			if (sourceStart < sourceEnd) {
+				assert targetStart == sourceEnd;
+				// something left from the source range
+				mergeRanges.add(markerSource, sourceStart, sourceEnd - sourceStart);
+			} else if (targetStart < sourceEnd) {
+				assert sourceStart == sourceEnd;
+				mergeRanges.add(markerTarget, targetStart, sourceEnd - targetStart);
+			}
+			return mergeRanges;
+		}
+
+		public int getLineInP2(int mergeLine) {
+			return matches.reverseMapLine(mergeLine);
+		}
+
+		public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2) {
+			return doCombine(start2, end2 - start2, source1, source2);
+		}
+
+		public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2) {
+			return doCombine(start, end - start, source1, source2);
+		}
+	}
+	
+	private static class MergeStrategy2 implements MergeResolutionStrategy {
+		// equal ranges in p2 and merged revision
+		private final List<RangePairSeq> matches; 
+		private final IntSliceSeq mergeRanges;
+		private final DiffRangeMap p1ToBase;
+		private final DiffRangeMap baseToP2;
+
+		public MergeStrategy2(List<RangePairSeq> p2EqualToM, DiffRangeMap p1ToBaseRanges, DiffRangeMap baseToP2Ranges) {
+			matches = p2EqualToM;
+			p1ToBase = p1ToBaseRanges;
+			baseToP2= baseToP2Ranges;
+			mergeRanges = new IntSliceSeq(3, 10, 10);
+		}
+
+
+		public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2) {
+			return combineAndMarkRangesWithSource(insPoint, insPoint, start, end, source1, source2);
+		}
+
+		public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2) {
+			mergeRanges.clear();
+			IntSliceSeq mergedLines = new IntSliceSeq(2, end2-start2, 0);
+			for (int i = start2; i < end2; i++) {
+				mergedLines.add(source1, 0);
+			}
+			// [s1Start..s1End) // range in p1 seen as changed in m
+			for (RangePair p1_b : p1ToBase.findInSource(start1, end1)) {
+				// there might be few ranges in (p1-base) that overlap with (p1-m) changes
+				for (RangePair b_p2 : baseToP2.findInSource(p1_b.start2(), p1_b.end2())) {
+					// regions in p2 that correspond to affected regions in base
+					for (int p2Line = b_p2.start2(); p2Line < b_p2.end2(); p2Line++) {
+						for (RangePairSeq eq : matches) {
+							if (eq.includesOriginLine(p2Line)) {
+								// this line in p2 is equal to some line in merge
+								int mergeLine = eq.mapLineIndex(p2Line);
+								if (mergeLine >= start2 && mergeLine < end2) {
+									mergedLines.set(mergeLine - start2, source2, p2Line);
+								}
+							}
+						}
+					}
+				}
+			}
+			int lineCount = 0, start = start2;
+			int lastSeenSource = source1;
+			for (IntTuple t : mergedLines) {
+				if (t.at(0) == lastSeenSource) {
+					lineCount++;
+				} else {
+					if (lineCount > 0) {
+						mergeRanges.add(lastSeenSource, start, lineCount);
+						start += lineCount;
+					}
+					lineCount = 1;
+					lastSeenSource = t.at(0);
+				}
+			}
+			if (lineCount > 0) {
+				mergeRanges.add(lastSeenSource, start, lineCount);
+			}
+			return mergeRanges;
+		}
+
+		public int getLineInP2(int mergeLine) {
+			for (RangePairSeq eq : matches) {
+				if (eq.includesTargetLine(mergeLine)) {
+					return eq.reverseMapLine(mergeLine);
+				}
+			}
+			return -1;
+		}
+	}
+
+
+	private static class AnnotateRev implements RevisionDescriptor {
+		public ContentBlock origin, target;
+		public int originCset, targetCset, mergeCset, fileRevIndex;
+		public HgDataFile df;
+		
+		public void set(HgDataFile file, int fileRev) {
+			df = file;
+			fileRevIndex = fileRev;
+		}
+		public void set(ContentBlock o, ContentBlock t) {
+			origin = o;
+			target = t;
+		}
+		public void set(int o, int t, int m) {
+			originCset = o;
+			targetCset = t;
+			mergeCset = m;
+		}
+		
+		public BlockData origin() {
+			return origin;
+		}
+
+		public BlockData target() {
+			return target;
+		}
+
+		public int originChangesetIndex() {
+			return originCset;
+		}
+
+		public int targetChangesetIndex() {
+			return targetCset;
+		}
+
+		public boolean isMerge() {
+			return mergeCset != NO_REVISION;
+		}
+
+		public int mergeChangesetIndex() {
+			return mergeCset;
+		}
+
+		public int fileRevisionIndex() {
+			return fileRevIndex;
+		}
+		public HgDataFile file() {
+			return df;
+		}
+		@Override
+		public String toString() {
+			if (isMerge()) {
+				return String.format("[%d,%d->%d]", originCset, mergeCset, targetCset);
+			}
+			return String.format("[%d->%d]", originCset, targetCset);
+		}
+	}
+
+	public static void main(String[] args) {
+		EqualBlocksCollector bc = new EqualBlocksCollector();
+		bc.match(-1, 5, 3);
+		bc.match(-1, 10, 2);
+		bc.match(-1, 15, 3);
+		bc.match(-1, 20, 3);
+		IntVector r = new IntVector();
+		bc.intersectWithTarget(7, 10, r);
+		for (int i = 0; i < r.size(); i+=2) {
+			System.out.printf("[%d..%d) ", r.get(i), r.get(i) + r.get(i+1));
+		}
+		System.out.println();
+		MergeStrategy1 ms = new MergeStrategy1(bc.matches);
+		IntSliceSeq mr = ms.doCombine(0, 16, 508, 514);
+		for (IntTuple t : mr) {
+			System.out.printf("%d:[%d..%d)  ", t.at(0), t.at(1), t.at(1) + t.at(2));
+		}
+		System.out.println();
+		System.out.println();
+		DiffRangeMap m1 = new DiffRangeMap(); // p1 -> base
+		m1.match(0, 0, 1); // =1..1 -> 1..1
+		m1.match(7, 3, 0);  // *2..7 -> 2..3
+		DiffRangeMap m2 = new DiffRangeMap(); // base -> p2
+		m2.match(0, 0, 1); // =1..1 -> 1..1
+		m2.match(3, 3, 0); // *2..3 -> 2..3
+		RangePairSeq eq1 = new RangePairSeq();
+		eq1.add(0, 0, 3);
+		RangePairSeq eq2 = new RangePairSeq();
+		eq2.add(0, 4, 3);
+		MergeStrategy2 ms2 = new MergeStrategy2(Arrays.asList(eq1, eq2), m1, m2);
+		mr = ms2.combineAndMarkRangesWithSource(5, 7, 5, 7, 33, 44);
+		for (IntTuple t : mr) {
+			System.out.printf("%d:[%d..%d)  ", t.at(0), t.at(1), t.at(1) + t.at(2));
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/diff/DiffHelper.java	Wed Aug 14 14:51:51 2013 +0200
@@ -0,0 +1,442 @@
+/*
+ * 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.diff;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.tmatesoft.hg.internal.IntMap;
+import org.tmatesoft.hg.internal.IntSliceSeq;
+import org.tmatesoft.hg.internal.IntTuple;
+import org.tmatesoft.hg.internal.IntVector;
+
+/**
+ * Mercurial cares about changes only up to the line level, e.g. a simple file version dump in manifest looks like (RevlogDump output):
+ * 
+ *   522:        233748      0        103      17438        433        522      521       -1     756073cf2321df44d3ed0585f2a5754bc8a1b2f6
+ *   <PATCH>:
+ *   3487..3578, 91:src/org/tmatesoft/hg/core/HgIterateDirection.java\00add61a8a665c5d8f092210767f812fe0d335ac8
+ *   
+ * I.e. for the {fname}{revision} entry format of manifest, not only {revision} is changed, but the whole line, with unchanged {fname} is recorded
+ * in the patch.
+ * 
+ * Mercurial paper describes reasons for choosing this approach to delta generation, too.
+ * 
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class DiffHelper<T extends DiffHelper.ChunkSequence<?>> {
+
+	private Map<Object, IntVector> chunk2UseIndex;
+	private T seq1, seq2;
+
+	// get filled by #longestMatch, track start of common sequence in seq1 and seq2, respectively
+	private int matchStartS1, matchStartS2;
+
+	private MatchInspector<T> matchInspector; 
+
+	public void init(T s1, T s2) {
+		seq1 = s1;
+		seq2 = s2;
+		prepare(s2);
+	}
+	
+	public void init(T s1) {
+		if (seq2 == null) {
+			throw new IllegalStateException("Use this #init() only when target sequence shall be matched against different origin");
+		}
+		seq1 = s1;
+	}
+
+
+	private void prepare(T s2) {
+		chunk2UseIndex = new HashMap<Object, IntVector>();
+		for (int i = 0, len = s2.chunkCount(); i < len; i++) {
+			Object bc = s2.chunk(i);
+			IntVector loc = chunk2UseIndex.get(bc);
+			if (loc == null) {
+				chunk2UseIndex.put(bc, loc = new IntVector());
+			}
+			loc.add(i);
+			// bc.registerUseIn(i) - BEWARE, use of bc here is incorrect
+			// in this case need to find the only ByteChain to keep indexes
+			// i.e. when there are few equal ByteChain instances, notion of "usedIn" shall be either shared (reference same vector)
+			// or kept within only one of them
+		}
+	}
+	
+	public void findMatchingBlocks(MatchInspector<T> insp) {
+		insp.begin(seq1, seq2);
+		matchInspector = insp;
+		findMatchingBlocks(0, seq1.chunkCount(), 0, seq2.chunkCount());
+		insp.end();
+	}
+	
+	/** 
+	 * look up every line in s2 that match lines in s1
+	 * idea: pure additions in s2 are diff-ed against s1 again and again, to see if there are any matches
+	 */
+	void findAllMatchAlternatives(final MatchInspector<T> insp) {
+		assert seq1.chunkCount() > 0;
+		final IntSliceSeq insertions = new IntSliceSeq(2);
+		final boolean matchedAny[] = new boolean[] {false};
+		DeltaInspector<T> myInsp = new DeltaInspector<T>() {
+			@Override
+			protected void unchanged(int s1From, int s2From, int length) {
+				matchedAny[0] = true;
+				insp.match(s1From, s2From, length);
+			}
+			@Override
+			protected void added(int s1InsertPoint, int s2From, int s2To) {
+				insertions.add(s2From, s2To);
+			}
+		};
+		matchInspector = myInsp;
+		myInsp.begin(seq1, seq2);
+		IntSliceSeq s2RangesToCheck = new IntSliceSeq(2, 1, 0);
+		s2RangesToCheck.add(0, seq2.chunkCount());
+		do {
+			IntSliceSeq nextCheck = new IntSliceSeq(2);
+			for (IntTuple t : s2RangesToCheck) {
+				int s2Start = t.at(0);
+				int s2End = t.at(1);
+				myInsp.changeStartS1 = 0;
+				myInsp.changeStartS2 = s2Start;
+				insp.begin(seq1, seq2);
+				matchedAny[0] = false;
+				findMatchingBlocks(0, seq1.chunkCount(), s2Start, s2End);
+				insp.end();
+				myInsp.end();
+				if (matchedAny[0]) {
+					nextCheck.addAll(insertions);
+				}
+				insertions.clear();
+			}
+			s2RangesToCheck = nextCheck;
+		} while (s2RangesToCheck.size() > 0);
+	}
+	
+	/**
+	 * implementation based on Python's difflib.py and SequenceMatcher 
+	 */
+	public int longestMatch(int startS1, int endS1, int startS2, int endS2) {
+		matchStartS1 = matchStartS2 = 0;
+		int maxLength = 0;
+		IntMap<Integer> chunkIndex2MatchCount = new IntMap<Integer>(8);
+		for (int i = startS1; i < endS1; i++) {
+			Object bc = seq1.chunk(i);
+			IntVector occurencesInS2 = chunk2UseIndex.get(bc);
+			if (occurencesInS2 == null) {
+				chunkIndex2MatchCount.clear();
+				continue;
+			}
+			IntMap<Integer> newChunkIndex2MatchCount = new IntMap<Integer>(8);
+			for (int j : occurencesInS2.toArray()) {
+				// s1[i] == s2[j]
+				if (j < startS2) {
+					continue;
+				}
+				if (j >= endS2) {
+					break;
+				}
+				int prevChunkMatches = chunkIndex2MatchCount.containsKey(j-1) ? chunkIndex2MatchCount.get(j-1) : 0;
+				int k = prevChunkMatches + 1;
+				newChunkIndex2MatchCount.put(j, k);
+				if (k > maxLength) {
+					matchStartS1 = i-k+1;
+					matchStartS2 = j-k+1;
+					maxLength = k;
+				}
+			}
+			chunkIndex2MatchCount = newChunkIndex2MatchCount;
+		}
+		return maxLength;
+	}
+	
+	private void findMatchingBlocks(int startS1, int endS1, int startS2, int endS2) {
+		int matchLength = longestMatch(startS1, endS1, startS2, endS2);
+		if (matchLength > 0) {
+			final int saveStartS1 = matchStartS1, saveStartS2 = matchStartS2;
+			if (startS1 < matchStartS1 && startS2 < matchStartS2) {
+				findMatchingBlocks(startS1, matchStartS1, startS2, matchStartS2);
+			}
+			matchInspector.match(saveStartS1, saveStartS2, matchLength);
+			if (saveStartS1+matchLength < endS1 && saveStartS2+matchLength < endS2) {
+				findMatchingBlocks(saveStartS1 + matchLength, endS1, saveStartS2 + matchLength, endS2);
+			}
+		}
+	}
+	
+	public interface MatchInspector<T extends ChunkSequence<?>> {
+		void begin(T s1, T s2);
+		void match(int startSeq1, int startSeq2, int matchLength);
+		void end();
+	}
+	
+	static class MatchDumpInspector<T extends ChunkSequence<?>> implements MatchInspector<T> {
+		private int matchCount;
+
+		public void begin(T s1, T s2) {
+			matchCount = 0;
+		}
+
+		public void match(int startSeq1, int startSeq2, int matchLength) {
+			matchCount++;
+			System.out.printf("match #%d: from line #%d  and line #%d of length %d\n", matchCount, startSeq1, startSeq2, matchLength);
+		}
+
+		public void end() {
+			if (matchCount == 0) {
+				System.out.println("NO MATCHES FOUND!");
+			}
+		}
+	}
+	
+	/**
+	 * Matcher implementation that translates "match/equal" notification to a delta-style "added/removed/changed". 
+	 */
+	public static class DeltaInspector<T extends ChunkSequence<?>> implements MatchInspector<T> {
+		protected int changeStartS1, changeStartS2;
+		protected T seq1, seq2;
+
+		public void begin(T s1, T s2) {
+			seq1 = s1;
+			seq2 = s2;
+			changeStartS1 = changeStartS2 = 0;
+		}
+
+		public void match(int startSeq1, int startSeq2, int matchLength) {
+			reportDeltaElement(startSeq1, startSeq2, matchLength);
+			changeStartS1 = startSeq1 + matchLength;
+			changeStartS2 = startSeq2 + matchLength;
+		}
+
+		public void end() {
+			if (changeStartS1 < seq1.chunkCount()-1 || changeStartS2 < seq2.chunkCount()-1) {
+				reportDeltaElement(seq1.chunkCount()-1, seq2.chunkCount()-1, 0);
+			}
+		}
+
+		protected void reportDeltaElement(int matchStartSeq1, int matchStartSeq2, int matchLength) {
+			if (changeStartS1 < matchStartSeq1) {
+				if (changeStartS2 < matchStartSeq2) {
+					changed(changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2);
+				} else {
+					assert changeStartS2 == matchStartSeq2;
+					deleted(matchStartSeq2, changeStartS1, matchStartSeq1);
+				}
+			} else {
+				assert changeStartS1 == matchStartSeq1;
+				if(changeStartS2 < matchStartSeq2) {
+					added(changeStartS1, changeStartS2, matchStartSeq2);
+				} else {
+					assert changeStartS2 == matchStartSeq2;
+					if (matchStartSeq1 > 0 || matchStartSeq2 > 0) {
+						assert false : String.format("adjustent equal blocks %d, %d and %d,%d", changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2);
+					}
+				}
+			}
+			if (matchLength > 0) {
+				unchanged(matchStartSeq1, matchStartSeq2, matchLength);
+			}
+		}
+
+		/**
+		 * [s1From..s1To) replaced with [s2From..s2To)
+		 */
+		protected void changed(int s1From, int s1To, int s2From, int s2To) {
+			// NO-OP
+		}
+
+		protected void deleted(int s2DeletePoint, int s1From, int s1To) {
+			// NO-OP
+		}
+
+		protected void added(int s1InsertPoint, int s2From, int s2To) {
+			// NO-OP
+		}
+
+		protected void unchanged(int s1From, int s2From, int length) {
+			// NO-OP
+		}
+	}
+	
+	public static class DeltaDumpInspector<T extends ChunkSequence<?>> extends DeltaInspector<T> {
+
+		@Override
+		protected void changed(int s1From, int s1To, int s2From, int s2To) {
+			System.out.printf("changed [%d..%d) with [%d..%d)\n", s1From, s1To, s2From, s2To);
+		}
+		
+		@Override
+		protected void deleted(int s2DeletionPoint, int s1From, int s1To) {
+			System.out.printf("deleted [%d..%d)\n", s1From, s1To);
+		}
+		
+		@Override
+		protected void added(int s1InsertPoint, int s2From, int s2To) {
+			System.out.printf("added [%d..%d) at %d\n", s2From, s2To, s1InsertPoint);
+		}
+
+		@Override
+		protected void unchanged(int s1From, int s2From, int length) {
+			System.out.printf("same [%d..%d) and [%d..%d)\n", s1From, s1From + length, s2From, s2From + length);
+		}
+	}
+	
+	/**
+	 * Generic sequence of chunk, where chunk is anything comparable to another chunk, e.g. a string or a single char
+	 * Sequence diff algorithm above doesn't care about sequence nature.
+	 */
+	public interface ChunkSequence<T> {
+		public T chunk(int index);
+		public int chunkCount();
+	}
+	
+	public static final class LineSequence implements ChunkSequence<LineSequence.ByteChain> {
+		
+		private final byte[] input;
+		private ArrayList<ByteChain> lines;
+
+		public LineSequence(byte[] data) {
+			input = data;
+		}
+		
+		public static LineSequence newlines(byte[] array) {
+			return new LineSequence(array).splitByNewlines();
+		}
+
+		// sequence ends with fake, empty line chunk
+		public LineSequence splitByNewlines() {
+			lines = new ArrayList<ByteChain>();
+			int lastStart = 0;
+			for (int i = 0; i < input.length; i++) {
+				if (input[i] == '\n') {
+					lines.add(new ByteChain(lastStart, i+1));
+					lastStart = i+1;
+				} else if (input[i] == '\r') {
+					if (i+1 < input.length && input[i+1] == '\n') {
+						i++;
+					}
+					lines.add(new ByteChain(lastStart, i+1));
+					lastStart = i+1;
+				}
+			}
+			if (lastStart < input.length) {
+				lines.add(new ByteChain(lastStart, input.length));
+			}
+			// empty chunk to keep offset of input end
+			lines.add(new ByteChain(input.length));
+			return this;
+		}
+		
+		public ByteChain chunk(int index) {
+			return lines.get(index);
+		}
+		
+		public int chunkCount() {
+			return lines.size();
+		}
+		
+		public byte[] data(int chunkFrom, int chunkTo) {
+			if (chunkFrom == chunkTo) {
+				return new byte[0];
+			}
+			int from = chunk(chunkFrom).getOffset(), to = chunk(chunkTo).getOffset();
+			byte[] rv = new byte[to - from];
+			System.arraycopy(input, from, rv, 0, rv.length);
+			return rv;
+		}
+
+		
+		public final class ByteChain {
+			private final int start, end;
+			private final int hash;
+			
+			/**
+			 * construct a chunk with a sole purpose to keep 
+			 * offset of the data end
+			 */
+			ByteChain(int offset) {
+				start = end = offset;
+				// ensure this chunk doesn't match trailing chunk of another sequence
+				hash = System.identityHashCode(this);
+			}
+			
+			ByteChain(int s, int e) {
+				start = s;
+				end = e;
+				hash = calcHash(input, s, e);
+			}
+			
+			/**
+			 * byte offset of the this ByteChain inside ChainSequence 
+			 */
+			public int getOffset() {
+				return start;
+			}
+			
+			public byte[] data() {
+				byte[] rv = new byte[end - start];
+				System.arraycopy(input, start, rv, 0, rv.length);
+				return rv;
+			}
+			
+			@Override
+			public boolean equals(Object obj) {
+				if (obj == null || obj.getClass() != ByteChain.class) {
+					return false;
+				}
+				ByteChain other = (ByteChain) obj;
+				if (other.hash != hash || other.end - other.start != end - start) {
+					return false;
+				}
+				return other.match(input, start);
+			}
+			
+			private boolean match(byte[] oi, int from) {
+				for (int i = start, j = from; i < end; i++, j++) {
+					if (LineSequence.this.input[i] != oi[j]) {
+						return false;
+					}
+				}
+				return true;
+			}
+			
+			@Override
+			public int hashCode() {
+				return hash;
+			}
+			
+			@Override
+			public String toString() {
+				return String.format("[@%d\"%s\"]", start, new String(data()));
+			}
+		}
+
+		// same as Arrays.hashCode(byte[]), just for a slice of a bigger array
+		static int calcHash(byte[] data, int from, int to) {
+			int result = 1;
+			for (int i = from; i < to; i++) {
+				result = 31 * result + data[i];
+			}
+			return result;
+		}
+	}
+}
--- a/src/org/tmatesoft/hg/internal/diff/DiffRangeMap.java	Thu Aug 08 21:32:22 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/diff/DiffRangeMap.java	Wed Aug 14 14:51:51 2013 +0200
@@ -18,11 +18,10 @@
 
 import java.util.ArrayList;
 
-import org.tmatesoft.hg.internal.DiffHelper;
-import org.tmatesoft.hg.internal.DiffHelper.MatchInspector;
 import org.tmatesoft.hg.internal.IntSliceSeq;
 import org.tmatesoft.hg.internal.IntTuple;
-import org.tmatesoft.hg.internal.DiffHelper.ChunkSequence;
+import org.tmatesoft.hg.internal.diff.DiffHelper.ChunkSequence;
+import org.tmatesoft.hg.internal.diff.DiffHelper.MatchInspector;
 
 /**
  * Sequence of pairs of ranges (s1Start,s1End) - (s2Start, s2End)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/diff/ForwardAnnotateInspector.java	Wed Aug 14 14:51:51 2013 +0200
@@ -0,0 +1,145 @@
+/*
+ * 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.diff;
+
+import org.tmatesoft.hg.core.HgAnnotateCommand.Inspector;
+import org.tmatesoft.hg.core.HgBlameInspector;
+import org.tmatesoft.hg.core.HgCallbackTargetException;
+import org.tmatesoft.hg.core.HgIterateDirection;
+import org.tmatesoft.hg.internal.IntMap;
+import org.tmatesoft.hg.internal.IntSliceSeq;
+import org.tmatesoft.hg.internal.IntTuple;
+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 (with actual file change) 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;
+		if (!all.containsKey(revision)) {
+			throw new IllegalArgumentException(String.format("Revision %d has not been visited", revision));
+		}
+		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++, lineInRev+1, 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 blockStart, 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 > blockStart) {
+				// part of origin block from blockStart up to originBlockEnd, but not more
+				// than size of the block (when blockStart is out of block start, i.e. < originPos)
+				int originBlockOverlap = Math.min(originBlockLen, originBlockEnd - blockStart);
+				assert originBlockOverlap > 0;
+				// eat as much as there's left in the block being copied
+				int originBlockConsumed = Math.min(originBlockOverlap, targetBlockLen);
+				int originBlockLine = t.at(2);
+				if (originPos < blockStart) {
+					originBlockLine += originBlockLen-originBlockOverlap;
+				}
+				// copy fragment of original block;
+				current.add(originBlockConsumed, t.at(1), originBlockLine);
+				targetBlockLen -= originBlockConsumed;
+				if (targetBlockLen == 0) {
+					break;
+				}
+			}
+			originPos += originBlockLen;
+		}
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/diff/LineImpl.java	Wed Aug 14 14:51:51 2013 +0200
@@ -0,0 +1,54 @@
+/*
+ * 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.diff;
+
+import org.tmatesoft.hg.core.HgAnnotateCommand.LineInfo;
+
+/**
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+final class LineImpl implements LineInfo {
+	private int ln;
+	private int origLine;
+	private int rev;
+	private byte[] content;
+
+	void init(int line, int firstAppearance, int csetRev, byte[] cnt) {
+		ln = line;
+		origLine = firstAppearance;
+		rev = csetRev;
+		content = cnt;
+	}
+
+	public int getLineNumber() {
+		return ln;
+	}
+
+
+	public int getOriginLineNumber() {
+		return origLine;
+	}
+
+	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/diff/RangePairSeq.java	Wed Aug 14 14:51:51 2013 +0200
@@ -0,0 +1,172 @@
+/*
+ * 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.diff;
+
+import java.util.Formatter;
+
+import org.tmatesoft.hg.internal.IntSliceSeq;
+import org.tmatesoft.hg.internal.IntTuple;
+
+/**
+ * Sequence of range pairs (denoted origin and target), {originStart, targetStart, length}, tailored for diff/annotate
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public final class RangePairSeq {
+	private final IntSliceSeq ranges = new IntSliceSeq(3);
+	
+	public void add(int start1, int start2, int length) {
+		int count = ranges.size();
+		if (count > 0) {
+			int lastS1 = ranges.get(--count, 0);
+			int lastS2 = ranges.get(count, 1);
+			int lastLen = ranges.get(count, 2);
+			if (start1 == lastS1 + lastLen && start2 == lastS2 + lastLen) {
+				// new range continues the previous one - just increase the length
+				ranges.set(count, lastS1, lastS2, lastLen + length);
+				return;
+			}
+		}
+		ranges.add(start1, start2, length);
+	}
+	
+	public void clear() {
+		ranges.clear();
+	}
+
+	public int size() {
+		return ranges.size();
+	}
+
+	/**
+	 * find out line index in the target that matches specified origin line
+	 */
+	public int mapLineIndex(int ln) {
+		for (IntTuple t : ranges) {
+			int s1 = t.at(0);
+			if (s1 > ln) {
+				return -1;
+			}
+			int l = t.at(2);
+			if (s1 + l > ln) {
+				int s2 = t.at(1);
+				return s2 + (ln - s1);
+			}
+		}
+		return -1;
+	}
+	
+	/**
+	 * find out line index in origin that matches specified target line
+	 */
+	public int reverseMapLine(int targetLine) {
+		for (IntTuple t : ranges) {
+			int ts = t.at(1);
+			if (ts > targetLine) {
+				return -1;
+			}
+			int l = t.at(2);
+			if (ts + l > targetLine) {
+				int os = t.at(0);
+				return os + (targetLine - ts);
+			}
+		}
+		return -1;
+	}
+	
+	public RangePairSeq intersect(RangePairSeq target) {
+		RangePairSeq v = new RangePairSeq();
+		for (IntTuple t : ranges) {
+			int originLine = t.at(0);
+			int targetLine = t.at(1);
+			int length = t.at(2);
+			int startTargetLine = -1, startOriginLine = -1, c = 0;
+			for (int j = 0; j < length; j++) {
+				int lnInFinal = target.mapLineIndex(targetLine + j);
+				if (lnInFinal == -1 || (startTargetLine != -1 && lnInFinal != startTargetLine + c)) {
+					// the line is not among "same" in ultimate origin
+					// or belongs to another/next "same" chunk 
+					if (startOriginLine == -1) {
+						continue;
+					}
+					v.add(startOriginLine, startTargetLine, c);
+					c = 0;
+					startOriginLine = startTargetLine = -1;
+					// fall-through to check if it's not complete miss but a next chunk
+				}
+				if (lnInFinal != -1) {
+					if (startOriginLine == -1) {
+						startOriginLine = originLine + j;
+						startTargetLine = lnInFinal;
+						c = 1;
+					} else {
+						// lnInFinal != startTargetLine + s is covered above
+						assert lnInFinal == startTargetLine + c;
+						c++;
+					}
+				}
+			}
+			if (startOriginLine != -1) {
+				assert c > 0;
+				v.add(startOriginLine, startTargetLine, c);
+			}
+		}
+		return v;
+	}
+	
+	// true when specified line in origin is equal to a line in target
+	public boolean includesOriginLine(int ln) {
+		return includes(ln, 0);
+	}
+	
+	// true when specified line in target is equal to a line in origin
+	public boolean includesTargetLine(int ln) {
+		return includes(ln, 1);
+	}
+
+	private boolean includes(int ln, int o) {
+		for (IntTuple t : ranges) {
+			int rangeStart = t.at(o);
+			if (rangeStart > ln) {
+				return false;
+			}
+			int rangeLen = t.at(2);
+			if (rangeStart + rangeLen > ln) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	public CharSequence dump() {
+		StringBuilder sb = new StringBuilder();
+		Formatter f = new Formatter(sb);
+		for (IntTuple t : ranges) {
+			int s1 = t.at(0);
+			int s2 = t.at(1);
+			int len = t.at(2);
+			f.format("[%d..%d) == [%d..%d);  ", s1, s1 + len, s2, s2 + len);
+		}
+		return sb;
+	}
+	
+	@Override
+	public String toString() {
+		return String.format("RangeSeq[%d]:%s", size(), dump());
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/diff/ReverseAnnotateInspector.java	Wed Aug 14 14:51:51 2013 +0200
@@ -0,0 +1,168 @@
+/*
+ * 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.diff;
+
+
+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.internal.IntMap;
+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;
+	private int[] lineNumbers;
+
+	/**
+	 * @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, lineNumbers[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 [knownLines.length];
+			Arrays.fill(lineRevisions, NO_REVISION);
+			lineNumbers = new int[knownLines.length];
+			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, ln, block.targetChangesetIndex());
+				}
+				knownLines[lnInFinal] = true;
+			}
+		}
+	}
+
+	public void changed(ChangeBlock block) {
+		added(block);
+	}
+
+	public void deleted(DeleteBlock block) {
+	}
+
+	private void line(int lineNumber, int firstAppearance, int changesetRevIndex) {
+		lineRevisions[lineNumber] = changesetRevIndex;
+		lineNumbers[lineNumber] = firstAppearance;
+	}
+}
\ No newline at end of file
--- a/test/org/tmatesoft/hg/test/TestAuxUtilities.java	Thu Aug 08 21:32:22 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestAuxUtilities.java	Wed Aug 14 14:51:51 2013 +0200
@@ -36,8 +36,8 @@
 import org.tmatesoft.hg.internal.IntTuple;
 import org.tmatesoft.hg.internal.IntVector;
 import org.tmatesoft.hg.internal.PathScope;
-import org.tmatesoft.hg.internal.RangePairSeq;
 import org.tmatesoft.hg.internal.RevisionDescendants;
+import org.tmatesoft.hg.internal.diff.RangePairSeq;
 import org.tmatesoft.hg.repo.HgChangelog;
 import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
 import org.tmatesoft.hg.repo.HgDataFile;
--- a/test/org/tmatesoft/hg/test/TestBlame.java	Thu Aug 08 21:32:22 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestBlame.java	Wed Aug 14 14:51:51 2013 +0200
@@ -45,9 +45,9 @@
 import org.tmatesoft.hg.core.HgDiffCommand;
 import org.tmatesoft.hg.core.HgRepoFacade;
 import org.tmatesoft.hg.core.Nodeid;
-import org.tmatesoft.hg.internal.ForwardAnnotateInspector;
 import org.tmatesoft.hg.internal.IntVector;
-import org.tmatesoft.hg.internal.ReverseAnnotateInspector;
+import org.tmatesoft.hg.internal.diff.ForwardAnnotateInspector;
+import org.tmatesoft.hg.internal.diff.ReverseAnnotateInspector;
 import org.tmatesoft.hg.repo.HgChangelog;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgLookup;
--- a/test/org/tmatesoft/hg/test/TestDiffHelper.java	Thu Aug 08 21:32:22 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestDiffHelper.java	Wed Aug 14 14:51:51 2013 +0200
@@ -17,12 +17,12 @@
 package org.tmatesoft.hg.test;
 
 import static org.junit.Assert.*;
-import static org.tmatesoft.hg.internal.DiffHelper.LineSequence.newlines;
+import static org.tmatesoft.hg.internal.diff.DiffHelper.LineSequence.newlines;
 
 import org.junit.Test;
-import org.tmatesoft.hg.internal.DiffHelper;
-import org.tmatesoft.hg.internal.DiffHelper.ChunkSequence;
-import org.tmatesoft.hg.internal.DiffHelper.LineSequence;
+import org.tmatesoft.hg.internal.diff.DiffHelper;
+import org.tmatesoft.hg.internal.diff.DiffHelper.ChunkSequence;
+import org.tmatesoft.hg.internal.diff.DiffHelper.LineSequence;
 import org.tmatesoft.hg.internal.IntVector;
 
 /**
--- a/test/org/tmatesoft/hg/test/TestRevlog.java	Thu Aug 08 21:32:22 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestRevlog.java	Wed Aug 14 14:51:51 2013 +0200
@@ -25,10 +25,10 @@
 import org.tmatesoft.hg.core.HgRepositoryNotFoundException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.internal.ByteArrayDataAccess;
-import org.tmatesoft.hg.internal.DiffHelper;
 import org.tmatesoft.hg.internal.Patch;
-import org.tmatesoft.hg.internal.DiffHelper.LineSequence;
 import org.tmatesoft.hg.internal.RevlogDump.RevlogReader;
+import org.tmatesoft.hg.internal.diff.DiffHelper;
+import org.tmatesoft.hg.internal.diff.DiffHelper.LineSequence;
 import org.tmatesoft.hg.repo.HgLookup;
 import org.tmatesoft.hg.repo.HgManifest;
 import org.tmatesoft.hg.repo.HgManifest.Flags;