changeset 548:ab21ac7dd833

Line-by-line annotation API and support code in place
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Mon, 18 Feb 2013 19:58:51 +0100
parents 66fc86e8c0dd
children 83afa680555d
files src/org/tmatesoft/hg/internal/AnnotateFacility.java src/org/tmatesoft/hg/internal/FileAnnotation.java test/org/tmatesoft/hg/test/FileAnnotation.java test/org/tmatesoft/hg/test/OutputParser.java test/org/tmatesoft/hg/test/TestBlame.java
diffstat 5 files changed, 255 insertions(+), 219 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/internal/AnnotateFacility.java	Mon Feb 18 19:58:10 2013 +0100
+++ b/src/org/tmatesoft/hg/internal/AnnotateFacility.java	Mon Feb 18 19:58:51 2013 +0100
@@ -17,11 +17,13 @@
 package org.tmatesoft.hg.internal;
 
 import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
+import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import org.tmatesoft.hg.core.Nodeid;
 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;
 
 /**
@@ -31,7 +33,27 @@
  */
 @Experimental(reason="work in progress")
 public class AnnotateFacility {
-	
+
+	/**
+	 * Annotate file revision, line by line. 
+	 */
+	public void annotate(HgDataFile df, int changesetRevisionIndex, LineInspector insp) {
+		if (!df.exists()) {
+			return;
+		}
+		Nodeid fileRev = df.getRepo().getManifest().getFileRevision(changesetRevisionIndex, df.getPath());
+		int fileRevIndex = df.getRevisionIndex(fileRev);
+		int[] fileRevParents = new int[2];
+		FileAnnotation fa = new FileAnnotation(insp);
+		do {
+			// also covers changesetRevisionIndex == TIP, #implAnnotateChange doesn't tolerate constants
+			changesetRevisionIndex = df.getChangesetRevisionIndex(fileRevIndex);
+			df.parents(fileRevIndex, fileRevParents, null, null);
+			implAnnotateChange(df, changesetRevisionIndex, fileRevIndex, fileRevParents, fa);
+			fileRevIndex = fileRevParents[0];
+		} while (fileRevIndex != NO_REVISION);
+	}
+
 	/**
 	 * Annotates changes of the file against its parent(s)
 	 */
@@ -41,22 +63,29 @@
 		int fileRevIndex = df.getRevisionIndex(fileRev);
 		int[] fileRevParents = new int[2];
 		df.parents(fileRevIndex, fileRevParents, null, null);
+		if (changesetRevisionIndex == TIP) {
+			changesetRevisionIndex = df.getChangesetRevisionIndex(fileRevIndex);
+		}
+		implAnnotateChange(df, changesetRevisionIndex, fileRevIndex, fileRevParents, insp);
+	}
+
+	private void implAnnotateChange(HgDataFile df, int csetRevIndex, int fileRevIndex, int[] fileParentRevs, BlockInspector insp) {
 		try {
-			if (fileRevParents[0] != NO_REVISION && fileRevParents[1] != NO_REVISION) {
+			if (fileParentRevs[0] != NO_REVISION && fileParentRevs[1] != NO_REVISION) {
 				// merge
-			} else if (fileRevParents[0] == fileRevParents[1]) {
+			} else if (fileParentRevs[0] == fileParentRevs[1]) {
 				// may be equal iff both are unset
-				assert fileRevParents[0] == NO_REVISION;
+				assert fileParentRevs[0] == NO_REVISION;
 				// everything added
 				ByteArrayChannel c;
 				df.content(fileRevIndex, c = new ByteArrayChannel());
-				BlameBlockInspector bbi = new BlameBlockInspector(insp, NO_REVISION, changesetRevisionIndex);
+				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 = fileRevParents[0] == NO_REVISION ? fileRevParents[1] : fileRevParents[0];
+				int soleParent = fileParentRevs[0] == NO_REVISION ? fileParentRevs[1] : fileParentRevs[0];
 				assert soleParent != NO_REVISION;
 				ByteArrayChannel c1, c2;
 				df.content(soleParent, c1 = new ByteArrayChannel());
@@ -64,7 +93,7 @@
 				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, changesetRevisionIndex));
+				pg.findMatchingBlocks(new BlameBlockInspector(insp, parentChangesetRevIndex, csetRevIndex));
 			}
 		} catch (CancelledException ex) {
 			// TODO likely it was bad idea to throw cancelled exception from content()
@@ -123,6 +152,9 @@
 	
 	@Callback
 	public interface LineInspector {
+		/**
+		 * Not necessarily invoked sequentially by line numbers
+		 */
 		void line(int lineNumber, int changesetRevIndex, LineDescriptor ld);
 	}
 	
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/FileAnnotation.java	Mon Feb 18 19:58:51 2013 +0100
@@ -0,0 +1,195 @@
+/*
+ * 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.LinkedList;
+
+import org.tmatesoft.hg.internal.AnnotateFacility.AddBlock;
+import org.tmatesoft.hg.internal.AnnotateFacility.ChangeBlock;
+import org.tmatesoft.hg.internal.AnnotateFacility.DeleteBlock;
+import org.tmatesoft.hg.internal.AnnotateFacility.EqualBlock;
+import org.tmatesoft.hg.internal.AnnotateFacility.LineInspector;
+
+
+/**
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class FileAnnotation implements AnnotateFacility.BlockInspectorEx {
+		// 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);
+		// 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 boolean[] knownLines;
+		private final LineInspector delegate;
+		
+		public FileAnnotation(AnnotateFacility.LineInspector lineInspector) {
+			delegate = lineInspector;
+		}
+		
+		public void start(int originLineCount, int targetLineCount) {
+			if (knownLines == null) {
+				knownLines = new boolean[targetLineCount];
+			}
+		}
+
+//		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() {
+			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);
+					int startTargetLine = -1, startOriginLine = -1, c = 0;
+					for (int j = 0; j < length; j++) {
+						int lnInFinal = 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);
+							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 {
+								assert lnInFinal == startTargetLine + c;
+								c++;
+							}
+						}
+					}
+					if (startOriginLine != -1) {
+						assert c > 0;
+						v.add(startOriginLine);
+						v.add(startTargetLine);
+						v.add(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 LineDescriptor());
+					knownLines[lnInFinal] = true;
+				}
+			}
+		}
+		
+		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;
+				}
+				// line >= b.firstRemovedLine
+				if (b.firstRemovedLine() + b.totalRemovedLines() > line) {
+					return true;
+				}
+			}
+			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 LineDescriptor implements AnnotateFacility.LineDescriptor {
+			LineDescriptor() {
+			}
+			
+			public int totalLines() {
+				return FileAnnotation.this.knownLines.length;
+			}
+		}
+	}
\ No newline at end of file
--- a/test/org/tmatesoft/hg/test/FileAnnotation.java	Mon Feb 18 19:58:10 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,196 +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.test;
-
-import java.util.LinkedList;
-
-import org.tmatesoft.hg.internal.AnnotateFacility;
-import org.tmatesoft.hg.internal.AnnotateFacility.LineInspector;
-import org.tmatesoft.hg.internal.IntVector;
-import org.tmatesoft.hg.internal.AnnotateFacility.AddBlock;
-import org.tmatesoft.hg.internal.AnnotateFacility.ChangeBlock;
-import org.tmatesoft.hg.internal.AnnotateFacility.DeleteBlock;
-import org.tmatesoft.hg.internal.AnnotateFacility.EqualBlock;
-
-/**
- * 
- * @author Artem Tikhomirov
- * @author TMate Software Ltd.
- */
-public class FileAnnotation implements AnnotateFacility.BlockInspectorEx {
-		// 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);
-		// 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 boolean[] knownLines;
-		private final LineInspector delegate;
-		
-		public FileAnnotation(AnnotateFacility.LineInspector lineInspector) {
-			delegate = lineInspector;
-		}
-		
-		public void start(int originLineCount, int targetLineCount) {
-			if (knownLines == null) {
-				knownLines = new boolean[targetLineCount];
-			}
-		}
-
-//		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() {
-			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);
-					int startTargetLine = -1, startOriginLine = -1, c = 0;
-					for (int j = 0; j < length; j++) {
-						int lnInFinal = 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);
-							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 {
-								assert lnInFinal == startTargetLine + c;
-								c++;
-							}
-						}
-					}
-					if (startOriginLine != -1) {
-						assert c > 0;
-						v.add(startOriginLine);
-						v.add(startTargetLine);
-						v.add(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 LineDescriptor());
-					knownLines[lnInFinal] = true;
-				}
-			}
-		}
-		
-		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;
-				}
-				// line >= b.firstRemovedLine
-				if (b.firstRemovedLine() + b.totalRemovedLines() > line) {
-					return true;
-				}
-			}
-			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 LineDescriptor implements AnnotateFacility.LineDescriptor {
-			LineDescriptor() {
-			}
-			
-			public int totalLines() {
-				return FileAnnotation.this.knownLines.length;
-			}
-		}
-	}
\ No newline at end of file
--- a/test/org/tmatesoft/hg/test/OutputParser.java	Mon Feb 18 19:58:10 2013 +0100
+++ b/test/org/tmatesoft/hg/test/OutputParser.java	Mon Feb 18 19:58:51 2013 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011-2012 TMate Software Ltd
+ * Copyright (c) 2011-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
@@ -50,6 +50,9 @@
 		public CharSequence result() {
 			return result;
 		}
+		public void reset() {
+			result = null;
+		}
 
 		public Iterable<String> lines() {
 			return lines(Pattern.compile("(.+)$", Pattern.MULTILINE), 1);
--- a/test/org/tmatesoft/hg/test/TestBlame.java	Mon Feb 18 19:58:10 2013 +0100
+++ b/test/org/tmatesoft/hg/test/TestBlame.java	Mon Feb 18 19:58:51 2013 +0100
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
+import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
@@ -67,25 +68,26 @@
 	public void testFileAnnotate() throws Exception {
 		HgRepository repo = new HgLookup().detectFromWorkingDir();
 		final String fname = "src/org/tmatesoft/hg/internal/PatchGenerator.java";
-		final int[] checkChangesets = new int[] { 539 , 536, 531 };
 		HgDataFile df = repo.getFileNode(fname);
-		AnnotateFacility af = new AnnotateFacility();
-		FileAnnotateInspector fa = new FileAnnotateInspector();
-		for (int cs : checkChangesets) {
-			af.annotateChange(df, cs, new FileAnnotation(fa));
-		}
-		
 		OutputParser.Stub op = new OutputParser.Stub();
 		ExecHelper eh = new ExecHelper(op, null);
-		eh.run("hg", "annotate", "-r", String.valueOf(checkChangesets[0]), 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);
 
-		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]);
+		for (int startChangeset : new int[] { 539, 541/*, TIP */}) {
+			FileAnnotateInspector fa = new FileAnnotateInspector();
+			new AnnotateFacility().annotate(df, startChangeset, 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);
+	
+			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]);
+			}
 		}
 	}