changeset 549:83afa680555d

Annotate merge revision (combined diff against two parents without looking further)
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 19 Feb 2013 21:17:39 +0100
parents ab21ac7dd833
children c1478cc31f45
files src/org/tmatesoft/hg/internal/AnnotateFacility.java src/org/tmatesoft/hg/internal/IntVector.java src/org/tmatesoft/hg/internal/PatchGenerator.java test/org/tmatesoft/hg/test/TestBlame.java
diffstat 4 files changed, 342 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/internal/AnnotateFacility.java	Mon Feb 18 19:58:51 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/AnnotateFacility.java	Tue Feb 19 21:17:39 2013 +0100
@@ -23,7 +23,6 @@
 import org.tmatesoft.hg.internal.PatchGenerator.LineSequence;
 import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgInvalidStateException;
-import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.util.CancelledException;
 
 /**
@@ -33,6 +32,19 @@
  */
 @Experimental(reason="work in progress")
 public class AnnotateFacility {
+	
+	/**
+	 * mimic 'hg diff -r csetRevIndex1 -r csetRevIndex2'
+	 */
+	public void diff(HgDataFile df, int csetRevIndex1, int csetRevIndex2, BlockInspector insp) {
+		int fileRevIndex1 = fileRevIndex(df, csetRevIndex1);
+		int fileRevIndex2 = fileRevIndex(df, csetRevIndex2);
+		LineSequence c1 = lines(df, fileRevIndex1);
+		LineSequence c2 = lines(df, fileRevIndex2);
+		PatchGenerator<LineSequence> pg = new PatchGenerator<LineSequence>();
+		pg.init(c1, c2);
+		pg.findMatchingBlocks(new BlameBlockInspector(insp, csetRevIndex1, csetRevIndex2));
+	}
 
 	/**
 	 * Annotate file revision, line by line. 
@@ -41,8 +53,7 @@
 		if (!df.exists()) {
 			return;
 		}
-		Nodeid fileRev = df.getRepo().getManifest().getFileRevision(changesetRevisionIndex, df.getPath());
-		int fileRevIndex = df.getRevisionIndex(fileRev);
+		int fileRevIndex = fileRevIndex(df, changesetRevisionIndex);
 		int[] fileRevParents = new int[2];
 		FileAnnotation fa = new FileAnnotation(insp);
 		do {
@@ -59,8 +70,7 @@
 	 */
 	public void annotateChange(HgDataFile df, int changesetRevisionIndex, BlockInspector insp) {
 		// TODO detect if file is text/binary (e.g. looking for chars < ' ' and not \t\r\n\f
-		Nodeid fileRev = df.getRepo().getManifest().getFileRevision(changesetRevisionIndex, df.getPath());
-		int fileRevIndex = df.getRevisionIndex(fileRev);
+		int fileRevIndex = fileRevIndex(df, changesetRevisionIndex);
 		int[] fileRevParents = new int[2];
 		df.parents(fileRevIndex, fileRevParents, null, null);
 		if (changesetRevisionIndex == TIP) {
@@ -70,31 +80,51 @@
 	}
 
 	private void implAnnotateChange(HgDataFile df, int csetRevIndex, int fileRevIndex, int[] fileParentRevs, BlockInspector insp) {
+		final LineSequence fileRevLines = lines(df, fileRevIndex);
+		if (fileParentRevs[0] != NO_REVISION && fileParentRevs[1] != NO_REVISION) {
+			LineSequence p1Lines = lines(df, fileParentRevs[0]);
+			LineSequence p2Lines = lines(df, fileParentRevs[1]);
+			int p1ClogIndex = df.getChangesetRevisionIndex(fileParentRevs[0]);
+			int p2ClogIndex = df.getChangesetRevisionIndex(fileParentRevs[1]);
+			PatchGenerator<LineSequence> pg = new PatchGenerator<LineSequence>();
+			pg.init(p2Lines, fileRevLines);
+			EqualBlocksCollector p2MergeCommon = new EqualBlocksCollector();
+			pg.findMatchingBlocks(p2MergeCommon);
+			//
+			pg.init(p1Lines);
+			BlameBlockInspector bbi = new BlameBlockInspector(insp, p1ClogIndex, csetRevIndex);
+			bbi.setMergeParent2(p2MergeCommon, p2ClogIndex);
+			pg.findMatchingBlocks(bbi);
+		} else if (fileParentRevs[0] == fileParentRevs[1]) {
+			// may be equal iff both are unset
+			assert fileParentRevs[0] == NO_REVISION;
+			// everything added
+			BlameBlockInspector bbi = new BlameBlockInspector(insp, NO_REVISION, csetRevIndex);
+			bbi.begin(LineSequence.newlines(new byte[0]), fileRevLines);
+			bbi.match(0, fileRevLines.chunkCount()-1, 0);
+			bbi.end();
+		} else {
+			int soleParent = fileParentRevs[0] == NO_REVISION ? fileParentRevs[1] : fileParentRevs[0];
+			assert soleParent != NO_REVISION;
+			LineSequence parentLines = lines(df, soleParent);
+			
+			int parentChangesetRevIndex = df.getChangesetRevisionIndex(soleParent);
+			PatchGenerator<LineSequence> pg = new PatchGenerator<LineSequence>();
+			pg.init(parentLines, fileRevLines);
+			pg.findMatchingBlocks(new BlameBlockInspector(insp, parentChangesetRevIndex, csetRevIndex));
+		}
+	}
+
+	private static int fileRevIndex(HgDataFile df, int csetRevIndex) {
+		Nodeid fileRev = df.getRepo().getManifest().getFileRevision(csetRevIndex, df.getPath());
+		return df.getRevisionIndex(fileRev);
+	}
+
+	private static LineSequence lines(HgDataFile df, int fileRevIndex) {
 		try {
-			if (fileParentRevs[0] != NO_REVISION && fileParentRevs[1] != NO_REVISION) {
-				// merge
-			} else if (fileParentRevs[0] == fileParentRevs[1]) {
-				// may be equal iff both are unset
-				assert fileParentRevs[0] == NO_REVISION;
-				// everything added
-				ByteArrayChannel c;
-				df.content(fileRevIndex, c = new ByteArrayChannel());
-				BlameBlockInspector bbi = new BlameBlockInspector(insp, NO_REVISION, csetRevIndex);
-				LineSequence cls = LineSequence.newlines(c.toArray());
-				bbi.begin(LineSequence.newlines(new byte[0]), cls);
-				bbi.match(0, cls.chunkCount()-1, 0);
-				bbi.end();
-			} else {
-				int soleParent = fileParentRevs[0] == NO_REVISION ? fileParentRevs[1] : fileParentRevs[0];
-				assert soleParent != NO_REVISION;
-				ByteArrayChannel c1, c2;
-				df.content(soleParent, c1 = new ByteArrayChannel());
-				df.content(fileRevIndex, c2 = new ByteArrayChannel());
-				int parentChangesetRevIndex = df.getChangesetRevisionIndex(soleParent);
-				PatchGenerator<LineSequence> pg = new PatchGenerator<LineSequence>();
-				pg.init(LineSequence.newlines(c1.toArray()), LineSequence.newlines(c2.toArray()));
-				pg.findMatchingBlocks(new BlameBlockInspector(insp, parentChangesetRevIndex, csetRevIndex));
-			}
+			ByteArrayChannel c;
+			df.content(fileRevIndex, c = new ByteArrayChannel());
+			return LineSequence.newlines(c.toArray());
 		} catch (CancelledException ex) {
 			// TODO likely it was bad idea to throw cancelled exception from content()
 			// deprecate and provide alternative?
@@ -166,16 +196,25 @@
 
 	static class BlameBlockInspector extends PatchGenerator.DeltaInspector<LineSequence> {
 		private final BlockInspector insp;
-		private final int csetP1;
+		private final int csetOrigin;
 		private final int csetTarget;
+		private EqualBlocksCollector p2MergeCommon;
+		private int csetMergeParent;
+		private IntVector mergeRanges;
 
-		public BlameBlockInspector(BlockInspector inspector, int parentCset1, int targetCset) {
+		public BlameBlockInspector(BlockInspector inspector, int originCset, int targetCset) {
 			assert inspector != null;
 			insp = inspector;
-			csetP1 = parentCset1;
+			csetOrigin = originCset;
 			csetTarget = targetCset;
 		}
 		
+		public void setMergeParent2(EqualBlocksCollector p2Merge, int parentCset2) {
+			p2MergeCommon = p2Merge;
+			csetMergeParent = parentCset2;
+			mergeRanges = new IntVector(3*10, 3*10);
+		}
+		
 		@Override
 		public void begin(LineSequence s1, LineSequence s2) {
 			super.begin(s1, s2);
@@ -194,31 +233,90 @@
 
 		@Override
 		protected void changed(int s1From, int s1To, int s2From, int s2To) {
-			BlockImpl2 block = new BlockImpl2(seq1, seq2, s1From, s1To-s1From, s2From, s2To - s2From, s1From, s2From);
-			block.setOriginAndTarget(csetP1, csetTarget);
-			insp.changed(block);
+			if (p2MergeCommon != null) {
+				mergeRanges.clear();
+				p2MergeCommon.combineAndMarkRangesWithTarget(s2From, s2To - s2From, csetOrigin, csetMergeParent, mergeRanges);
+				
+				/*
+				 * Usecases:
+				 * 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) 
+				 */
+				int s1TotalLines = s1To - s1From, s1ConsumedLines = 0, s1Start = s1From;
+				
+				for (int i = 0; i < mergeRanges.size(); i += 3) {
+					final int rangeOrigin = mergeRanges.get(i);
+					final int rangeStart = mergeRanges.get(i+1);
+					final int rangeLen = mergeRanges.get(i+2);
+					final boolean lastRange = i+3 >= mergeRanges.size();
+					final int s1LinesLeft = s1TotalLines - s1ConsumedLines;
+					// how many lines we may reported 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 (s1LinesToBorrow > 0) {
+						BlockImpl2 block = new BlockImpl2(seq1, seq2, s1Start, s1LinesToBorrow, rangeStart, rangeLen, s1Start, rangeStart);
+						block.setOriginAndTarget(rangeOrigin, csetTarget);
+						insp.changed(block);
+						s1ConsumedLines += s1LinesToBorrow;
+						s1Start += s1LinesToBorrow;
+					} else {
+						BlockImpl2 block = getAddBlock(rangeStart, rangeLen, s1Start);
+						block.setOriginAndTarget(rangeOrigin, csetTarget);
+						insp.added(block);
+					}
+				}
+				if (s1ConsumedLines != s1TotalLines) {
+					throw new HgInvalidStateException(String.format("Expected to process %d lines, but actually was %d", s1TotalLines, s1ConsumedLines));
+				}
+			} else {
+				BlockImpl2 block = new BlockImpl2(seq1, seq2, s1From, s1To-s1From, s2From, s2To - s2From, s1From, s2From);
+				block.setOriginAndTarget(csetOrigin, csetTarget);
+				insp.changed(block);
+			}
 		}
 		
 		@Override
 		protected void added(int s1InsertPoint, int s2From, int s2To) {
-			BlockImpl2 block = new BlockImpl2(null, seq2, -1, -1, s2From, s2To - s2From, s1InsertPoint, -1);
-			block.setOriginAndTarget(csetP1, csetTarget);
-			insp.added(block);
+			if (p2MergeCommon != null) {
+				mergeRanges.clear();
+				p2MergeCommon.combineAndMarkRangesWithTarget(s2From, s2To - s2From, csetOrigin, csetMergeParent, mergeRanges);
+				int insPoint = s1InsertPoint; // track changes to insertion point
+				for (int i = 0; i < mergeRanges.size(); i += 3) {
+					int rangeOrigin = mergeRanges.get(i);
+					int rangeStart = mergeRanges.get(i+1);
+					int rangeLen = mergeRanges.get(i+2);
+					BlockImpl2 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 {
+				BlockImpl2 block = getAddBlock(s2From, s2To - s2From, s1InsertPoint);
+				block.setOriginAndTarget(csetOrigin, csetTarget);
+				insp.added(block);
+			}
 		}
 		
 		@Override
 		protected void deleted(int s2DeletePoint, int s1From, int s1To) {
 			BlockImpl2 block = new BlockImpl2(seq1, null, s1From, s1To - s1From, -1, -1, -1, s2DeletePoint);
-			block.setOriginAndTarget(csetP1, csetTarget);
+			block.setOriginAndTarget(csetOrigin, csetTarget);
 			insp.deleted(block);
 		}
 
 		@Override
 		protected void unchanged(int s1From, int s2From, int length) {
 			BlockImpl1 block = new BlockImpl1(s1From, s2From, length);
-			block.setOriginAndTarget(csetP1, csetTarget);
+			block.setOriginAndTarget(csetOrigin, csetTarget);
 			insp.same(block);
 		}
+		
+		private BlockImpl2 getAddBlock(int start, int len, int insPoint) {
+			return new BlockImpl2(null, seq2, -1, -1, start, len, insPoint, -1);
+		}
 	}
 	
 	static class BlockImpl implements Block {
@@ -344,4 +442,132 @@
 			return String.format("@@ -%d,%d +%d,%d @@", firstRemovedLine(), totalRemovedLines(), firstAddedLine(), totalAddedLines());
 		}
 	}
+
+	static class EqualBlocksCollector implements PatchGenerator.MatchInspector<LineSequence> {
+		private final IntVector matches = new IntVector(10*3, 2*3);
+
+		public void begin(LineSequence s1, LineSequence s2) {
+		}
+
+		public void match(int startSeq1, int startSeq2, int matchLength) {
+			matches.add(startSeq1);
+			matches.add(startSeq2);
+			matches.add(matchLength);
+		}
+
+		public void end() {
+		}
+		
+		// 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);
+		}
+		
+		public void intersectWithTarget(int start, int length, IntVector result) {
+			int s = start;
+			for (int l = start, x = start + length; l < x; l++) {
+				if (!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);
+			}
+		}
+		
+		/*
+		 * 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)
+		 */
+		public void combineAndMarkRangesWithTarget(int start, int length, int markerSource, int markerTarget, IntVector result) {
+			int sourceStart = start, targetStart = start, sourceEnd = start + length;
+			for (int l = sourceStart; l < sourceEnd; l++) {
+				if (includesTargetLine(l)) {
+					// l is from target
+					if (sourceStart < l) {
+						// few lines from source range were not in the target, report them
+						result.add(markerSource);
+						result.add(sourceStart);
+						result.add(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
+						result.add(markerTarget);
+						result.add(targetStart);
+						result.add(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
+				result.add(markerSource);
+				result.add(sourceStart);
+				result.add(sourceEnd - sourceStart);
+			} else if (targetStart < sourceEnd) {
+				assert sourceStart == sourceEnd;
+				result.add(markerTarget);
+				result.add(targetStart);
+				result.add(sourceEnd - targetStart);
+			}
+		}
+		
+		private boolean includes(int ln, int o) {
+			for (int i = 2; i < matches.size(); o += 3, i+=3) {
+				int rangeStart = matches.get(o);
+				if (rangeStart > ln) {
+					return false;
+				}
+				int rangeLen = matches.get(i);
+				if (rangeStart + rangeLen > ln) {
+					return true;
+				}
+			}
+			return false;
+		}
+	}
+
+	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);
+		assert !bc.includesTargetLine(4);
+		assert bc.includesTargetLine(7);
+		assert !bc.includesTargetLine(8);
+		assert bc.includesTargetLine(10);
+		assert !bc.includesTargetLine(12);
+		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();
+		r.clear();
+		bc.combineAndMarkRangesWithTarget(0, 16, 508, 514, r);
+		for (int i = 0; i < r.size(); i+=3) {
+			System.out.printf("%d:[%d..%d)  ", r.get(i), r.get(i+1), r.get(i+1) + r.get(i+2));
+		}
+	}
 }
--- a/src/org/tmatesoft/hg/internal/IntVector.java	Mon Feb 18 19:58:51 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/IntVector.java	Tue Feb 19 21:17:39 2013 +0100
@@ -105,4 +105,9 @@
 		System.arraycopy(data, 0, newData, 0, count);
 		data = newData;
 	}
+	
+	@Override
+	public String toString() {
+		return String.format("%s[%d]", IntVector.class.getSimpleName(), size());
+	}
 }
--- a/src/org/tmatesoft/hg/internal/PatchGenerator.java	Mon Feb 18 19:58:51 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/PatchGenerator.java	Tue Feb 19 21:17:39 2013 +0100
@@ -21,6 +21,7 @@
 import java.util.Map;
 
 import org.tmatesoft.hg.repo.HgDataFile;
+import org.tmatesoft.hg.repo.HgInvalidStateException;
 import org.tmatesoft.hg.repo.HgLookup;
 import org.tmatesoft.hg.repo.HgRepository;
 
@@ -55,6 +56,13 @@
 		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) {
@@ -200,7 +208,11 @@
 					added(changeStartS1, changeStartS2, matchStartSeq2);
 				} else {
 					assert changeStartS2 == matchStartSeq2;
-					System.out.printf("adjustent equal blocks %d, %d and %d,%d\n", changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2);
+					if (matchStartSeq1 > 0 || matchStartSeq2 > 0) {
+						// FIXME perhaps, exception is too much for the case
+						// once diff is covered with tests, replace with assert false : msg; 
+						throw new HgInvalidStateException(String.format("adjustent equal blocks %d, %d and %d,%d", changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2));
+					}
 				}
 			}
 			if (matchLength > 0) {
--- a/test/org/tmatesoft/hg/test/TestBlame.java	Mon Feb 18 19:58:51 2013 +0100
+++ b/test/org/tmatesoft/hg/test/TestBlame.java	Tue Feb 19 21:17:39 2013 +0100
@@ -27,9 +27,12 @@
 import java.util.regex.Pattern;
 
 import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
+import org.tmatesoft.hg.console.Bundle.Dump;
 import org.tmatesoft.hg.internal.AnnotateFacility;
 import org.tmatesoft.hg.internal.AnnotateFacility.AddBlock;
+import org.tmatesoft.hg.internal.AnnotateFacility.Block;
 import org.tmatesoft.hg.internal.AnnotateFacility.ChangeBlock;
 import org.tmatesoft.hg.internal.AnnotateFacility.DeleteBlock;
 import org.tmatesoft.hg.internal.AnnotateFacility.EqualBlock;
@@ -46,6 +49,9 @@
  */
 public class TestBlame {
 
+	@Rule
+	public ErrorCollectorExt errorCollector = new ErrorCollectorExt();
+
 	
 	@Test
 	public void testSingleParentBlame() throws Exception {
@@ -72,7 +78,7 @@
 		OutputParser.Stub op = new OutputParser.Stub();
 		ExecHelper eh = new ExecHelper(op, null);
 
-		for (int startChangeset : new int[] { 539, 541/*, TIP */}) {
+		for (int startChangeset : new int[] { TIP, /*539, 541/ *, TIP */}) {
 			FileAnnotateInspector fa = new FileAnnotateInspector();
 			new AnnotateFacility().annotate(df, startChangeset, fa);
 			
@@ -86,7 +92,7 @@
 	
 			for (int i = 0; i < fa.lineRevisions.length; i++) {
 				int hgAnnotateRevIndex = Integer.parseInt(hgAnnotateLines[i].substring(0, hgAnnotateLines[i].indexOf(':')));
-				assertEquals(String.format("Revision mismatch for line %d", i+1), hgAnnotateRevIndex, fa.lineRevisions[i]);
+				errorCollector.assertEquals(String.format("Revision mismatch for line %d", i+1), hgAnnotateRevIndex, fa.lineRevisions[i]);
 			}
 		}
 	}
@@ -116,19 +122,49 @@
 		return rv;
 	}
 	
-	private void leftovers() throws Exception {
+	
+	private void aaa() throws Exception {
 		HgRepository repo = new HgLookup().detectFromWorkingDir();
 		final String fname = "src/org/tmatesoft/hg/internal/PatchGenerator.java";
 		final int checkChangeset = 539;
 		HgDataFile df = repo.getFileNode(fname);
 		AnnotateFacility af = new AnnotateFacility();
+		DiffOutInspector dump = new DiffOutInspector(System.out);
+		System.out.println("541 -> 543");
+		af.annotateChange(df, 543, dump);
+		System.out.println("539 -> 541");
+		af.annotateChange(df, 541, dump);
 		System.out.println("536 -> 539");
-		af.annotateChange(df, checkChangeset, new DiffOutInspector(System.out));
+		af.annotateChange(df, checkChangeset, dump);
 		System.out.println("531 -> 536");
-		af.annotateChange(df, 536, new DiffOutInspector(System.out));
+		af.annotateChange(df, 536, dump);
 		System.out.println(" -1 -> 531");
-		af.annotateChange(df, 531, new DiffOutInspector(System.out));
+		af.annotateChange(df, 531, dump);
+		
+		FileAnnotateInspector fai = new FileAnnotateInspector();
+		af.annotate(df, TIP, fai);
+		for (int i = 0; i < fai.lineRevisions.length; i++) {
+			System.out.printf("%3d: LINE %d\n", fai.lineRevisions[i], i+1);
+		}
+	}
 
+	private void bbb() throws Exception {
+		HgRepository repo = new HgLookup().detectFromWorkingDir();
+		final String fname = "src/org/tmatesoft/hg/repo/HgManifest.java";
+		final int checkChangeset = 415;
+		HgDataFile df = repo.getFileNode(fname);
+		AnnotateFacility af = new AnnotateFacility();
+		DiffOutInspector dump = new DiffOutInspector(System.out);
+		System.out.println("413 -> 415");
+		af.diff(df, 413, 415, dump);
+		System.out.println("408 -> 415");
+		af.diff(df, 408, 415, dump);
+		System.out.println("Combined (with merge):");
+		dump.needRevisions(true);
+		af.annotateChange(df, checkChangeset, dump);
+	}
+
+	private void leftovers() throws Exception {
 		IntMap<String> linesOld = new IntMap<String>(100), linesNew = new IntMap<String>(100);
 		System.out.println("Changes to old revision:");
 		for (int i = linesOld.firstKey(), x = linesOld.lastKey(); i < x; i++) {
@@ -150,21 +186,34 @@
 //		System.out.println(Arrays.equals(new String[] { "abc" }, splitLines("abc")));
 //		System.out.println(Arrays.equals(new String[] { "a", "bc" }, splitLines("a\nbc")));
 //		System.out.println(Arrays.equals(new String[] { "a", "bc" }, splitLines("a\nbc\n")));
-		new TestBlame().testFileAnnotate();
+		new TestBlame().bbb();
 	}
 
 	static class DiffOutInspector implements AnnotateFacility.BlockInspector {
 		private final PrintStream out;
+		private boolean dumpRevs;
 		
 		DiffOutInspector(PrintStream ps) {
 			out = ps;
 		}
 		
+		public void needRevisions(boolean dumpRevs) {
+			// Note, true makes output incompatible with 'hg diff'
+			this.dumpRevs = dumpRevs;
+		}
+		
+		private void printRevs(Block b) {
+			if (dumpRevs) {
+				out.printf("[%3d -> %3d] ", b.originChangesetIndex(), b.targetChangesetIndex());
+			}
+		}
+		
 		public void same(EqualBlock block) {
 			// nothing 
 		}
 		
 		public void deleted(DeleteBlock block) {
+			printRevs(block);
 			out.printf("@@ -%d,%d +%d,0 @@\n", block.firstRemovedLine() + 1, block.totalRemovedLines(), block.removedAt());
 //			String[] lines = block.removedLines();
 //			assert lines.length == block.totalRemovedLines();
@@ -176,10 +225,12 @@
 		public void changed(ChangeBlock block) {
 //			deleted(block);
 //			added(block);
+			printRevs(block);
 			out.printf("@@ -%d,%d +%d,%d @@\n", block.firstRemovedLine() + 1, block.totalRemovedLines(), block.firstAddedLine() + 1, block.totalAddedLines());
 		}
 		
 		public void added(AddBlock block) {
+			printRevs(block);
 			out.printf("@@ -%d,0 +%d,%d @@\n", block.insertedAt(), block.firstAddedLine() + 1, block.totalAddedLines());
 //			String[] addedLines = block.addedLines();
 //			assert addedLines.length == block.totalAddedLines();