changeset 557:b9e5ac26dd83

Annotate: Line annotation needs true line position from merged blocks; test-annotate repo updated to show elements from both parents in the merged revision
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Sun, 24 Feb 2013 00:11:40 +0100
parents e55f17a7a195
children 154718ae23ed
files src/org/tmatesoft/hg/core/HgCallbackTargetException.java src/org/tmatesoft/hg/internal/FileAnnotation.java src/org/tmatesoft/hg/repo/HgBlameFacility.java test-data/test-repos.jar test/org/tmatesoft/hg/test/TestBlame.java
diffstat 5 files changed, 303 insertions(+), 153 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgCallbackTargetException.java	Fri Feb 22 20:21:24 2013 +0100
+++ b/src/org/tmatesoft/hg/core/HgCallbackTargetException.java	Sun Feb 24 00:11:40 2013 +0100
@@ -32,8 +32,8 @@
  * <p>It's intentionally not a subclass of {@link HgException} to avoid get mixed with library own errors and be processed separately.
  * 
  * <p>Top-level API handlers ({@link HgStatusHandler}, {@link HgManifestHandler}, {@link HgChangesetHandler}, etc) allow to throw 
- * HgCallbackTargetException from their methods. Exceptions throws this way are not handled in corresponding commands, except for
- * revision or file name specification, unless already set. The, these exceptions go straight to the command caller.
+ * HgCallbackTargetException from their methods. Exceptions thrown this way are not handled in corresponding commands, except for
+ * revision or file name specification, unless already set. Then, these exceptions go straight to the command caller.
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
--- a/src/org/tmatesoft/hg/internal/FileAnnotation.java	Fri Feb 22 20:21:24 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/FileAnnotation.java	Sun Feb 24 00:11:40 2013 +0100
@@ -16,12 +16,18 @@
  */
 package org.tmatesoft.hg.internal;
 
-import java.util.LinkedList;
+import java.util.Formatter;
 
 import org.tmatesoft.hg.core.HgIterateDirection;
 import org.tmatesoft.hg.repo.HgBlameFacility;
+import org.tmatesoft.hg.repo.HgInvalidStateException;
+import org.tmatesoft.hg.repo.HgBlameFacility.AddBlock;
+import org.tmatesoft.hg.repo.HgBlameFacility.BlockData;
+import org.tmatesoft.hg.repo.HgBlameFacility.ChangeBlock;
+import org.tmatesoft.hg.repo.HgBlameFacility.DeleteBlock;
+import org.tmatesoft.hg.repo.HgBlameFacility.EqualBlock;
+import org.tmatesoft.hg.repo.HgBlameFacility.RevisionDescriptor;
 import org.tmatesoft.hg.repo.HgDataFile;
-import org.tmatesoft.hg.repo.HgBlameFacility.*;
 
 /**
  * Produce output like 'hg annotate' does
@@ -37,7 +43,7 @@
 		/**
 		 * Not necessarily invoked sequentially by line numbers
 		 */
-		void line(int lineNumber, int changesetRevIndex, LineDescriptor ld);
+		void line(int lineNumber, int changesetRevIndex, BlockData lineContent, LineDescriptor ld);
 	}
 
 	public interface LineDescriptor {
@@ -56,60 +62,168 @@
 		af.annotate(df, changelogRevisionIndex, fa, HgIterateDirection.NewToOld);
 	}
 
-	// blocks deleted in the target, as reported at the previous step
-	private LinkedList<DeleteBlock> deleted = new LinkedList<DeleteBlock>();
-	// blocks deleted in the origin, to become deletions in target at the next step
-	private LinkedList<DeleteBlock> newDeleted = new LinkedList<DeleteBlock>();
-	// keeps <startSeq1, startSeq2, len> of equal blocks, origin to target, from previous step
-	// XXX smth like IntSliceVector to access triples (or slices of any size, in fact)
-	// with easy indexing, e.g. #get(sliceIndex, indexWithinSlice)
-	// and vect.get(7,2) instead of vect.get(7*SIZEOF_SLICE+2)
-	private IntVector identical = new IntVector(20 * 3, 2 * 3);
+	// keeps <startSeq1, startSeq2, len> of equal blocks, origin to target, from some previous step
+	private RangeSeq activeEquals;
 	// equal blocks of the current iteration, to be recalculated before next step
 	// to track line number (current target to ultimate target) mapping 
-	private IntVector newIdentical = new IntVector(20 * 3, 2 * 3);
+	private RangeSeq intermediateEquals = new RangeSeq();
 
 	private boolean[] knownLines;
 	private final LineInspector delegate;
+	private RevisionDescriptor revisionDescriptor;
+	private BlockData lineContent;
+
+	private IntMap<RangeSeq> mergedRanges = new IntMap<RangeSeq>(10);
+	private IntMap<RangeSeq> equalRanges = new IntMap<RangeSeq>(10);
+	private boolean activeEqualsComesFromMerge = false;
 
 	public FileAnnotation(LineInspector lineInspector) {
 		delegate = lineInspector;
 	}
 
 	public void start(RevisionDescriptor rd) {
+		revisionDescriptor = rd;
 		if (knownLines == null) {
-			knownLines = new boolean[rd.target().elementCount()];
+			lineContent = rd.target();
+			knownLines = new boolean[lineContent.elementCount()];
+			activeEquals = new RangeSeq();
+			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()));
+				}
+			}
 		}
 	}
 
-	//		private static void ppp(IntVector v) {
-	//			for (int i = 0; i < v.size(); i+= 3) {
-	//				int len = v.get(i+2);
-	//				System.out.printf("[%d..%d) == [%d..%d);  ", v.get(i), v.get(i) + len, v.get(i+1), v.get(i+1) + len);
-	//			}
-	//			System.out.println();
-	//		}
+	public void done(RevisionDescriptor rd) {
+		// update line numbers of the intermediate target to point to ultimate target's line numbers
+		RangeSeq v = intermediateEquals.intersect(activeEquals);
+		if (activeEqualsComesFromMerge) {
+			mergedRanges.put(rd.originChangesetIndex(), v);
+		} else {
+			equalRanges.put(rd.originChangesetIndex(), v);
+		}
+		intermediateEquals.clear();
+		activeEquals = null;
+		activeEqualsComesFromMerge = false;
+		revisionDescriptor = null;
+	}
 
-	public void done(RevisionDescriptor rd) {
-		if (identical.size() > 0) {
-			// update line numbers of the intermediate target to point to ultimate target's line numbers
-			IntVector v = new IntVector(identical.size(), 2 * 3);
-			for (int i = 0; i < newIdentical.size(); i += 3) {
-				int originLine = newIdentical.get(i);
-				int targetLine = newIdentical.get(i + 1);
-				int length = newIdentical.get(i + 2);
+	public void same(EqualBlock block) {
+		intermediateEquals.add(block.originStart(), block.targetStart(), block.length());
+	}
+
+	public void added(AddBlock block) {
+		RangeSeq rs = null;
+		if (revisionDescriptor.isMerge() && block.originChangesetIndex() == revisionDescriptor.mergeChangesetIndex()) {
+			rs = mergedRanges.get(revisionDescriptor.mergeChangesetIndex());
+			if (rs == null) {
+				mergedRanges.put(revisionDescriptor.mergeChangesetIndex(), rs = new RangeSeq());
+			}
+		}
+		if (activeEquals.size() == 0) {
+			return;
+		}
+		for (int i = 0, ln = block.firstAddedLine(), x = block.totalAddedLines(); i < x; i++, ln++) {
+			int lnInFinal = activeEquals.mapLineIndex(ln);
+			if (lnInFinal != -1/* && !knownLines[lnInFinal]*/) {
+				if (rs != null) {
+					rs.add(block.insertedAt() + i, lnInFinal, 1);
+				} else {
+					delegate.line(lnInFinal, block.targetChangesetIndex(), lineContent.elementAt(lnInFinal), new LineDescriptorImpl());
+				}
+				knownLines[lnInFinal] = true;
+			}
+		}
+	}
+
+	public void changed(ChangeBlock block) {
+		added(block);
+	}
+
+	public void deleted(DeleteBlock block) {
+	}
+
+	private final class LineDescriptorImpl implements LineDescriptor {
+		LineDescriptorImpl() {
+		}
+
+		public int totalLines() {
+			return FileAnnotation.this.knownLines.length;
+		}
+	}
+
+	private static class RangeSeq {
+		// XXX smth like IntSliceVector to access triples (or slices of any size, in fact)
+		// with easy indexing, e.g. #get(sliceIndex, indexWithinSlice)
+		// and vect.get(7,2) instead of vect.get(7*SIZEOF_SLICE+2)
+		private final IntVector ranges = new IntVector(3*10, 3*5);
+		private int count;
+		
+		public void add(int start1, int start2, int length) {
+			if (count > 0) {
+				int lastIndex = 3 * (count-1);
+				int lastS1 = ranges.get(lastIndex);
+				int lastS2 = ranges.get(lastIndex + 1);
+				int lastLen = ranges.get(lastIndex + 2);
+				if (start1 == lastS1 + lastLen && start2 == lastS2 + lastLen) {
+					// new range continues the previous one - just increase the length
+					ranges.set(lastIndex + 2, lastLen + length);
+					return;
+				}
+			}
+			ranges.add(start1, start2, length);
+			count++;
+		}
+		
+		public void clear() {
+			ranges.clear();
+			count = 0;
+		}
+
+		public int size() {
+			return count;
+		}
+
+		public int mapLineIndex(int ln) {
+			for (int i = 0; i < ranges.size(); i += 3) {
+				int s1 = ranges.get(i);
+				if (s1 > ln) {
+					return -1;
+				}
+				int l = ranges.get(i+2);
+				if (s1 + l > ln) {
+					int s2 = ranges.get(i + 1);
+					return s2 + (ln - s1);
+				}
+			}
+			return -1;
+		}
+		
+		public RangeSeq intersect(RangeSeq target) {
+			RangeSeq v = new RangeSeq();
+			for (int i = 0; i < ranges.size(); i += 3) {
+				int originLine = ranges.get(i);
+				int targetLine = ranges.get(i + 1);
+				int length = ranges.get(i + 2);
 				int startTargetLine = -1, startOriginLine = -1, c = 0;
 				for (int j = 0; j < length; j++) {
-					int lnInFinal = mapLineIndex(targetLine + 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);
-						v.add(startTargetLine);
-						v.add(c);
+						v.add(startOriginLine, startTargetLine, c);
 						c = 0;
 						startOriginLine = startTargetLine = -1;
 						// fall-through to check if it's not complete miss but a next chunk
@@ -120,6 +234,7 @@
 							startTargetLine = lnInFinal;
 							c = 1;
 						} else {
+							// lnInFinal != startTargetLine + s is covered above
 							assert lnInFinal == startTargetLine + c;
 							c++;
 						}
@@ -127,94 +242,28 @@
 				}
 				if (startOriginLine != -1) {
 					assert c > 0;
-					v.add(startOriginLine);
-					v.add(startTargetLine);
-					v.add(c);
+					v.add(startOriginLine, startTargetLine, c);
 				}
 			}
-			newIdentical.clear();
-			identical = v;
-		} else {
-			IntVector li = newIdentical;
-			newIdentical = identical;
-			identical = li;
-		}
-		LinkedList<DeleteBlock> ld = newDeleted;
-		deleted.clear();
-		newDeleted = deleted;
-		deleted = ld;
-	}
-
-	public void same(EqualBlock block) {
-		newIdentical.add(block.originStart());
-		newIdentical.add(block.targetStart());
-		newIdentical.add(block.length());
-	}
-
-	public void added(AddBlock block) {
-		for (int i = 0, ln = block.firstAddedLine(), x = block.totalAddedLines(); i < x; i++, ln++) {
-			int lnInFinal = mapLineIndex(ln);
-			if (lnInFinal != -1 && !knownLines[lnInFinal]) {
-				delegate.line(lnInFinal, block.targetChangesetIndex(), new LineDescriptorImpl());
-				knownLines[lnInFinal] = true;
-			}
+			return v;
 		}
-	}
-
-	public void changed(ChangeBlock block) {
-		deleted(block);
-		added(block);
-	}
-
-	public void deleted(DeleteBlock block) {
-		newDeleted.add(block);
-	}
-
-	// line - index in the target
-	private boolean isDeleted(int line) {
-		for (DeleteBlock b : deleted) {
-			if (b.firstRemovedLine() > line) {
-				break;
+		
+		@SuppressWarnings("unused")
+		public CharSequence dump() {
+			StringBuilder sb = new StringBuilder();
+			Formatter f = new Formatter(sb);
+			for (int i = 0; i < ranges.size(); i += 3) {
+				int s1 = ranges.get(i);
+				int s2 = ranges.get(i + 1);
+				int len = ranges.get(i + 2);
+				f.format("[%d..%d) == [%d..%d);  ", s1, s1 + len, s2, s2 + len);
 			}
-			// line >= b.firstRemovedLine
-			if (b.firstRemovedLine() + b.totalRemovedLines() > line) {
-				return true;
-			}
+			return sb;
 		}
-		return false;
-	}
-
-	// map target lines to the lines of the revision being annotated (the one that came first)
-	private int mapLineIndex(int ln) {
-		if (isDeleted(ln)) {
-			return -1;
-		}
-		if (identical.isEmpty()) {
-			return ln;
-		}
-		for (int i = 0; i < identical.size(); i += 3) {
-			final int originStart = identical.get(i);
-			if (originStart > ln) {
-				//					assert false;
-				return -1;
-			}
-			// ln >= b.originStart
-			final int length = identical.get(i + 2);
-			if (originStart + length > ln) {
-				int targetStart = identical.get(i + 1);
-				return targetStart + (ln - originStart);
-			}
-		}
-		//			assert false;
-		return -1;
-	}
-
-	private final class LineDescriptorImpl implements LineDescriptor {
-		LineDescriptorImpl() {
-		}
-
-		public int totalLines() {
-			return FileAnnotation.this.knownLines.length;
+		
+		@Override
+		public String toString() {
+			return String.format("RangeSeq[%d]:%s", count, dump());
 		}
 	}
 }
\ No newline at end of file
--- a/src/org/tmatesoft/hg/repo/HgBlameFacility.java	Fri Feb 22 20:21:24 2013 +0100
+++ b/src/org/tmatesoft/hg/repo/HgBlameFacility.java	Sun Feb 24 00:11:40 2013 +0100
@@ -442,12 +442,15 @@
 				p2MergeCommon.combineAndMarkRangesWithTarget(s2From, s2To - s2From, csetOrigin, csetMergeParent, mergeRanges);
 				
 				/*
-				 * Usecases:
+				 * 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) 
+				 * 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;
 				
@@ -457,22 +460,30 @@
 					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)
+					// 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 (s1LinesToBorrow > 0) {
+					if (rangeOrigin != csetMergeParent && s1LinesToBorrow > 0) {
 						ChangeBlockImpl block = getChangeBlock(s1Start, s1LinesToBorrow, rangeStart, rangeLen);
 						block.setOriginAndTarget(rangeOrigin, csetTarget);
 						insp.changed(block);
 						s1ConsumedLines += s1LinesToBorrow;
 						s1Start += s1LinesToBorrow;
 					} else {
-						ChangeBlockImpl block = getAddBlock(rangeStart, rangeLen, s1Start);
+						int blockInsPoint = rangeOrigin != csetMergeParent ? s1Start : p2MergeCommon.reverseMapLine(rangeStart);
+						ChangeBlockImpl block = getAddBlock(rangeStart, rangeLen, blockInsPoint);
 						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));
+					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);
@@ -730,6 +741,7 @@
 	
 
 	static class EqualBlocksCollector implements DiffHelper.MatchInspector<LineSequence> {
+		// FIXME replace with RangeSeq
 		private final IntVector matches = new IntVector(10*3, 2*3);
 
 		public void begin(LineSequence s1, LineSequence s2) {
@@ -771,6 +783,25 @@
 			}
 		}
 		
+		/**
+		 * find out line index in origin that matches specifid target line
+		 */
+		public int reverseMapLine(int targetLine) {
+			for (int i = 0; i < matches.size(); i +=3) {
+				int os = matches.get(i);
+				int ts = matches.get(i + 1);
+				int l = matches.get(i + 2);
+				if (ts > targetLine) {
+					return -1;
+				}
+				if (ts + l > targetLine) {
+					return os + (targetLine - ts);
+				}
+			}
+			return -1;
+		}
+
+
 		/*
 		 * 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
@@ -877,6 +908,13 @@
 		public int fileRevisionIndex() {
 			return fileRevIndex;
 		}
+		@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) {
Binary file test-data/test-repos.jar has changed
--- a/test/org/tmatesoft/hg/test/TestBlame.java	Fri Feb 22 20:21:24 2013 +0100
+++ b/test/org/tmatesoft/hg/test/TestBlame.java	Sun Feb 24 00:11:40 2013 +0100
@@ -22,10 +22,13 @@
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.PrintStream;
 import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.junit.Assert;
@@ -37,15 +40,15 @@
 import org.tmatesoft.hg.internal.FileAnnotation.LineInspector;
 import org.tmatesoft.hg.internal.IntVector;
 import org.tmatesoft.hg.repo.HgBlameFacility;
-import org.tmatesoft.hg.repo.HgDataFile;
-import org.tmatesoft.hg.repo.HgLookup;
-import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.HgBlameFacility.AddBlock;
 import org.tmatesoft.hg.repo.HgBlameFacility.Block;
 import org.tmatesoft.hg.repo.HgBlameFacility.BlockData;
 import org.tmatesoft.hg.repo.HgBlameFacility.ChangeBlock;
 import org.tmatesoft.hg.repo.HgBlameFacility.DeleteBlock;
 import org.tmatesoft.hg.repo.HgBlameFacility.EqualBlock;
+import org.tmatesoft.hg.repo.HgDataFile;
+import org.tmatesoft.hg.repo.HgLookup;
+import org.tmatesoft.hg.repo.HgRepository;
 
 /**
  * 
@@ -56,6 +59,7 @@
 
 	@Rule
 	public ErrorCollectorExt errorCollector = new ErrorCollectorExt();
+	private ExecHelper eh;
 
 	
 	@Test
@@ -76,29 +80,46 @@
 	}
 	
 	@Test
-	public void testFileAnnotate() throws Exception {
+	public void testFileLineAnnotate1() throws Exception {
 		HgRepository repo = new HgLookup().detectFromWorkingDir();
 		final String fname = "src/org/tmatesoft/hg/internal/PatchGenerator.java";
 		HgDataFile df = repo.getFileNode(fname);
 		OutputParser.Stub op = new OutputParser.Stub();
-		ExecHelper eh = new ExecHelper(op, null);
+		eh = new ExecHelper(op, null);
 
 		for (int startChangeset : new int[] { 539, 541 /*, TIP */}) {
-			FileAnnotateInspector fa = new FileAnnotateInspector();
-			FileAnnotation.annotate(df, startChangeset, fa);
-			
+			doLineAnnotateTest(df, startChangeset, op);
+		}
+	}
+	
+	private void doLineAnnotateTest(HgDataFile df, int cs, OutputParser.Stub op) throws InterruptedException, IOException {
+		FileAnnotateInspector fa = new FileAnnotateInspector();
+		FileAnnotation.annotate(df, cs, fa);
 
-			op.reset();
-			eh.run("hg", "annotate", "-r", startChangeset == TIP ? "tip" : String.valueOf(startChangeset), fname);
-			
-			String[] hgAnnotateLines = splitLines(op.result());
-			assertTrue("[sanity]", hgAnnotateLines.length > 0);
-			assertEquals("Number of lines reported by native annotate and our impl", hgAnnotateLines.length, fa.lineRevisions.length);
+		op.reset();
+		eh.run("hg", "annotate", "-r", cs == TIP ? "tip" : String.valueOf(cs), df.getPath().toString());
+
+		String[] hgAnnotateLines = splitLines(op.result());
+		assertTrue("[sanity]", hgAnnotateLines.length > 0);
+		assertEquals("Number of lines reported by native annotate and our impl", hgAnnotateLines.length, fa.lineRevisions.length);
+
+		for (int i = 0; i < fa.lineRevisions.length; i++) {
+			int hgAnnotateRevIndex = Integer.parseInt(hgAnnotateLines[i].substring(0, hgAnnotateLines[i].indexOf(':')));
+			errorCollector.assertEquals(String.format("Revision mismatch for line %d", i+1), hgAnnotateRevIndex, fa.lineRevisions[i]);
+			String hgAnnotateLine = hgAnnotateLines[i].substring(hgAnnotateLines[i].indexOf(':') + 1);
+			String apiLine = fa.line(i).trim();
+			errorCollector.assertEquals(hgAnnotateLine.trim(), apiLine);
+		}
+	}
 	
-			for (int i = 0; i < fa.lineRevisions.length; i++) {
-				int hgAnnotateRevIndex = Integer.parseInt(hgAnnotateLines[i].substring(0, hgAnnotateLines[i].indexOf(':')));
-				errorCollector.assertEquals(String.format("Revision mismatch for line %d", i+1), hgAnnotateRevIndex, fa.lineRevisions[i]);
-			}
+	@Test
+	public void testFileLineAnnotate2() throws Exception {
+		HgRepository repo = Configuration.get().find("test-annotate");
+		HgDataFile df = repo.getFileNode("file1");
+		OutputParser.Stub op = new OutputParser.Stub();
+		eh = new ExecHelper(op, repo.getWorkingDir());
+		for (int cs : new int[] { 4, 6, TIP/*, 8 FIXME find out how come hg annotate doesn't see re-added line in rev4*/}) {
+			doLineAnnotateTest(df, cs, op);
 		}
 	}
 	
@@ -112,6 +133,41 @@
 		af.annotate(df, TIP, dump, HgIterateDirection.OldToNew);
 		LinkedList<String> apiResult = new LinkedList<String>(Arrays.asList(splitLines(bos.toString())));
 		
+		/*
+		 * FIXME this is an ugly hack to deal with the way `hg diff -c <mergeRev>` describes the change
+		 * and our merge handling approach. For merged revision m, and lines changed both in p1 and p2
+		 * we report lines from p2 as pure additions, regardless of intersecting p1 changes (which
+		 * are reported as deletions, if no sufficient changed lines in m found)
+		 * So, here we try to combine deletion that follows a change (based on identical insertionPoint)
+		 * into a single change
+		 * To fix, need to find better approach to find out reference info (i.e. `hg diff -c` is flawed in this case,
+		 * as it uses first parent only).
+		 */
+		Pattern fix = Pattern.compile("@@ -(\\d+),(\\d+) \\+(\\d+),(\\d+) @@");
+		int v1, v2, v3, v4;
+		v1 = v2 = v3 = v4 = -1;
+		for (ListIterator<String> it = apiResult.listIterator(); it.hasNext();) {
+			String n = it.next();
+			Matcher m = fix.matcher(n);
+			if (m.find()) {
+				int d1 = Integer.parseInt(m.group(1));
+				int d2 = Integer.parseInt(m.group(2));
+				int d3 = Integer.parseInt(m.group(3));
+				int d4 = Integer.parseInt(m.group(4));
+				if (v1 == d1 && d4 == 0) {
+					it.previous(); // shift to current element
+					it.previous(); // to real previous
+					it.remove();
+					it.next();
+					it.set(String.format("@@ -%d,%d +%d,%d @@", v1, v2+d2, v3, v4));
+				}
+				v1 = d1;
+				v2 = d2;
+				v3 = d3;
+				v4 = d4;
+			}
+		}
+		
 		LineGrepOutputParser gp = new LineGrepOutputParser("^@@.+");
 		ExecHelper eh = new ExecHelper(gp, repo.getWorkingDir());
 		for (int cs : dump.getReportedTargetRevisions()) {
@@ -199,18 +255,18 @@
 		HgRepository repo = new HgLookup().detect("/home/artem/hg/junit-test-repos/test-annotate/");
 		HgDataFile df = repo.getFileNode("file1");
 		HgBlameFacility af = new HgBlameFacility();
-		DiffOutInspector dump = new DiffOutInspector(System.out);
-		dump.needRevisions(true);
-		af.annotate(df, TIP, dump, HgIterateDirection.OldToNew);
-		System.out.println();
-		af.annotate(df, TIP, new LineDumpInspector(true), HgIterateDirection.NewToOld);
-		System.out.println();
-		af.annotate(df, TIP, new LineDumpInspector(false), HgIterateDirection.NewToOld);
-		System.out.println();
+//		DiffOutInspector dump = new DiffOutInspector(System.out);
+//		dump.needRevisions(true);
+//		af.annotate(df, TIP, dump, HgIterateDirection.OldToNew);
+//		System.out.println();
+//		af.annotate(df, TIP, new LineDumpInspector(true), HgIterateDirection.NewToOld);
+//		System.out.println();
+//		af.annotate(df, TIP, new LineDumpInspector(false), HgIterateDirection.NewToOld);
+//		System.out.println();
 		FileAnnotateInspector fa = new FileAnnotateInspector();
-		FileAnnotation.annotate(df, TIP, fa);
+		FileAnnotation.annotate(df, TIP, fa); //4,6,TIP
 		for (int i = 0; i < fa.lineRevisions.length; i++) {
-			System.out.printf("%d: LINE %d\n", fa.lineRevisions[i], i+1);
+			System.out.printf("%d: %s", fa.lineRevisions[i], fa.line(i) == null ? "null\n" : fa.line(i));
 		}
 	}
 
@@ -312,16 +368,23 @@
 
 	private static class FileAnnotateInspector implements LineInspector {
 		private int[] lineRevisions;
+		private String[] lines;
 		
 		FileAnnotateInspector() {
 		}
 		
-		public void line(int lineNumber, int changesetRevIndex, LineDescriptor ld) {
+		public void line(int lineNumber, int changesetRevIndex, BlockData lineContent, LineDescriptor ld) {
 			if (lineRevisions == null) {
 				lineRevisions = new int [ld.totalLines()];
 				Arrays.fill(lineRevisions, NO_REVISION);
+				lines = new String[ld.totalLines()];
 			}
 			lineRevisions[lineNumber] = changesetRevIndex;
+			lines[lineNumber] = new String(lineContent.asArray());
+		}
+		
+		public String line(int i) {
+			return lines[i];
 		}
 	}