diff src/org/tmatesoft/hg/internal/AnnotateFacility.java @ 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 4ea0351ca878
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));
+		}
+	}
 }