changeset 573:e49f9d9513fa

Partial blame when start/end revisions are in the middle of a single filename history
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Fri, 12 Apr 2013 19:50:21 +0200
parents becd2a1310a2
children 88afffd39899
files src/org/tmatesoft/hg/internal/IntVector.java src/org/tmatesoft/hg/repo/HgBlameFacility.java test/org/tmatesoft/hg/test/TestBlame.java
diffstat 3 files changed, 91 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/internal/IntVector.java	Fri Apr 12 18:30:55 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/IntVector.java	Fri Apr 12 19:50:21 2013 +0200
@@ -84,6 +84,13 @@
 		count = 0;
 	}
 	
+	public void trimTo(int newSize) {
+		if (newSize < 0 || newSize > count) {
+			throw new IllegalArgumentException(String.format("Can't trim vector of size %d to %d", count, newSize));
+		}
+		count = newSize;
+	}
+	
 	public void trimToSize() {
 		data = toArray(true);
 	}
--- a/src/org/tmatesoft/hg/repo/HgBlameFacility.java	Fri Apr 12 18:30:55 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgBlameFacility.java	Fri Apr 12 19:50:21 2013 +0200
@@ -79,7 +79,7 @@
 		if (wrongRevisionIndex(changelogRevIndexStart) || wrongRevisionIndex(changelogRevIndexEnd)) {
 			throw new IllegalArgumentException();
 		}
-		// Note, changelogRevisionIndex may be TIP, while the code below doesn't tolerate constants
+		// Note, changelogRevIndexEnd may be TIP, while the code below doesn't tolerate constants
 		//
 		int lastRevision = df.getRepo().getChangelog().getLastRevision();
 		if (changelogRevIndexEnd == TIP) {
@@ -101,8 +101,18 @@
 			fileCompleteHistory.addFirst(fileHistory); // to get the list in old-to-new order
 			nextChunk = fileHistory;
 			bh.useFileUpTo(currentFile, fileLastClogRevIndex);
-			if (currentFile.isCopy()) {
-				// TODO SessionContext.getPathFactory() and replace all Path.create
+			if (fileHistory.changeset(0) > changelogRevIndexStart && currentFile.isCopy()) {
+				// fileHistory.changeset(0) is the earliest revision we know about so far,
+				// once we get to revisions earlier than the requested start, stop digging.
+				// The reason there's NO == (i.e. not >=) because:
+				// (easy): once it's equal, we've reached our intended start
+				// (hard): if changelogRevIndexStart happens to be exact start of one of renames in the 
+				// chain of renames (test-annotate2 repository, file1->file1a->file1b, i.e. points 
+				// to the very start of file1a or file1 history), presence of == would get us to the next 
+				// chunk and hence changed parents of present chunk's first element. Our annotate alg 
+				// relies on parents only (i.e. knows nothing about 'last iteration element') to find out 
+				// what to compare, and hence won't report all lines of 'last iteration element' (which is the
+				// first revision of the renamed file) as "added in this revision", leaving gaps in annotate
 				HgRepository repo = currentFile.getRepo();
 				Nodeid originLastRev = currentFile.getCopySourceRevision();
 				currentFile = repo.getFileNode(currentFile.getCopySourceName());
@@ -110,9 +120,10 @@
 				// XXX perhaps, shall fail with meaningful exception if new file doesn't exist (.i/.d not found for whatever reason)
 				// or source revision is missing?
 			} else {
+				fileHistory.chopAtChangeset(changelogRevIndexStart);
 				currentFile = null; // stop iterating
 			}
-		} while (currentFile != null && fileLastClogRevIndex >= changelogRevIndexStart);
+		} while (currentFile != null && fileLastClogRevIndex > changelogRevIndexStart);
 		// fileCompleteHistory is in (origin, intermediate target, ultimate target) order
 
 		int[] fileClogParentRevs = new int[2];
@@ -406,7 +417,30 @@
 			target.originFileRev = fileRevsToVisit.get(0); // files to visit are new to old
 			target.originChangelogRev = changeset(target.originFileRev);
 		}
-		
+
+		/**
+		 * Mark revision closest(ceil) to specified as the very first one (no parents) 
+		 */
+		public void chopAtChangeset(int firstChangelogRevOfInterest) {
+			if (firstChangelogRevOfInterest == 0) {
+				return; // nothing to do
+			}
+			int i = 0, x = fileRevsToVisit.size(), fileRev = BAD_REVISION;
+			// fileRevsToVisit is new to old, greater numbers to smaller
+			while (i < x && changeset(fileRev = fileRevsToVisit.get(i)) >= firstChangelogRevOfInterest) {
+				i++;
+			}
+			assert fileRev != BAD_REVISION; // there's at least 1 revision in fileRevsToVisit
+			if (i == x && changeset(fileRev) != firstChangelogRevOfInterest) {
+				assert false : "Requested changeset shall belong to the chunk";
+				return;
+			}
+			fileRevsToVisit.trimTo(i); // no need to iterate more
+			// pretend fileRev got no parents
+			fileParentRevs.set(fileRev * 2, NO_REVISION);
+			fileParentRevs.set(fileRev, NO_REVISION);
+		}
+
 		public int[] fileRevisions(HgIterateDirection iterateOrder) {
 			// fileRevsToVisit is { r10, r7, r6, r5, r0 }, new to old
 			int[] rv = fileRevsToVisit.toArray();
--- a/test/org/tmatesoft/hg/test/TestBlame.java	Fri Apr 12 18:30:55 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestBlame.java	Fri Apr 12 19:50:21 2013 +0200
@@ -18,11 +18,15 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.tmatesoft.hg.core.HgIterateDirection.NewToOld;
+import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew;
 import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
 import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -186,6 +190,39 @@
 		errorCollector.assertTrue(String.format("Annotate API reported excessive diff: %s ", apiResult.toString()), apiResult.isEmpty());
 	}
 
+	
+	@Test
+	public void testPartialHistoryFollow() throws Exception {
+		HgRepository repo = Configuration.get().find("test-annotate2");
+		HgDataFile df = repo.getFileNode("file1b.txt");
+		// rev3: file1 -> file1a,  rev7: file1a -> file1b, tip: rev10
+		HgBlameFacility bf = new HgBlameFacility(df);
+		DiffOutInspector insp = new DiffOutInspector(new PrintStream(new OutputStream() {
+			@Override
+			public void write(int b) throws IOException {
+				// NULL OutputStream
+			}
+		}));
+		// rev6 changes rev4, rev4 changes rev3. Plus, anything changed 
+		// earlier than rev2 shall be reported as new from change3
+		int[] change_2_8_new2old = new int[] {4, 6, 3, 4, -1, 3}; 
+		int[] change_2_8_old2new = new int[] {-1, 3, 3, 4, 4, 6 };
+		bf.annotate(2, 8, insp, NewToOld);
+		Assert.assertArrayEquals(change_2_8_new2old, insp.getReportedRevisionPairs());
+		insp.reset();
+		bf.annotate(2, 8, insp, OldToNew);
+		Assert.assertArrayEquals(change_2_8_old2new, insp.getReportedRevisionPairs());
+		// same as 2 to 8, with addition of rev9 changes rev7  (rev6 to rev7 didn't change content, only name)
+		int[] change_3_9_new2old = new int[] {7, 9, 4, 6, 3, 4, -1, 3 }; 
+		int[] change_3_9_old2new = new int[] {-1, 3, 3, 4, 4, 6, 7, 9 };
+		insp.reset();
+		bf.annotate(3, 9, insp, NewToOld);
+		Assert.assertArrayEquals(change_3_9_new2old, insp.getReportedRevisionPairs());
+		insp.reset();
+		bf.annotate(3, 9, insp, OldToNew);
+		Assert.assertArrayEquals(change_3_9_old2new, insp.getReportedRevisionPairs());
+	}
+
 	@Test
 	public void testAnnotateCmdFollowNoFollow() throws Exception {
 		HgRepoFacade hgRepoFacade = new HgRepoFacade();
@@ -369,6 +406,14 @@
 			return x;
 		}
 		
+		int[] getReportedRevisionPairs() {
+			return reportedRevisionPairs.toArray();
+		}
+		
+		void reset() {
+			reportedRevisionPairs.clear();
+		}
+		
 		public void same(EqualBlock block) {
 			// nothing 
 		}