changeset 518:0d5e1ea7955e

Tests for HgLogCommand#execute(HgChangesetHandler) with various combination of follow renames and ancestry
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Thu, 20 Dec 2012 19:55:45 +0100
parents 9922d1f7cb2a
children 934037edbea0
files src/org/tmatesoft/hg/core/HgFileRevision.java src/org/tmatesoft/hg/core/HgLogCommand.java test/org/tmatesoft/hg/test/TestHistory.java
diffstat 3 files changed, 270 insertions(+), 65 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgFileRevision.java	Tue Dec 18 19:08:00 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgFileRevision.java	Thu Dec 20 19:55:45 2012 +0100
@@ -69,7 +69,7 @@
 		origin = orig; 
 	}
 	
-	public HgFileRevision(HgDataFile fileNode, Nodeid fileRevision, Path origin) {
+	HgFileRevision(HgDataFile fileNode, Nodeid fileRevision, Path origin) {
 		this(fileNode.getRepo(), fileRevision, null, fileNode.getPath(), origin); 
 	}
 	
--- a/src/org/tmatesoft/hg/core/HgLogCommand.java	Tue Dec 18 19:08:00 2012 +0100
+++ b/src/org/tmatesoft/hg/core/HgLogCommand.java	Thu Dec 20 19:55:45 2012 +0100
@@ -16,6 +16,7 @@
  */
 package org.tmatesoft.hg.core;
 
+import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 import static org.tmatesoft.hg.util.LogFacility.Severity.Error;
 
@@ -35,6 +36,7 @@
 
 import org.tmatesoft.hg.internal.IntMap;
 import org.tmatesoft.hg.internal.IntVector;
+import org.tmatesoft.hg.internal.Lifecycle;
 import org.tmatesoft.hg.repo.HgChangelog;
 import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
 import org.tmatesoft.hg.repo.HgDataFile;
@@ -65,7 +67,7 @@
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
  */
-public class HgLogCommand extends HgAbstractCommand<HgLogCommand> implements HgChangelog.Inspector {
+public class HgLogCommand extends HgAbstractCommand<HgLogCommand> {
 
 	private final HgRepository repo;
 	private Set<String> users;
@@ -276,6 +278,14 @@
 		if (csetTransform != null) {
 			throw new ConcurrentModificationException();
 		}
+		final int lastCset = endRev == TIP ? repo.getChangelog().getLastRevision() : endRev;
+		// XXX pretty much like HgInternals.checkRevlogRange
+		if (lastCset < 0 || lastCset > repo.getChangelog().getLastRevision()) {
+			throw new HgBadArgumentException(String.format("Bad value %d for end revision", endRev), null);
+		}
+		if (startRev < 0 || startRev > lastCset) {
+			throw new HgBadArgumentException(String.format("Bad value %d for start revision for range [%1$d..%d]", startRev, lastCset), null);
+		}
 		final ProgressSupport progressHelper = getProgressSupport(handler);
 		try {
 			count = 0;
@@ -283,40 +293,59 @@
 			// ChangesetTransfrom creates a blank PathPool, and #file(String, boolean) above 
 			// may utilize it as well. CommandContext? How about StatusCollector there as well?
 			csetTransform = new ChangesetTransformer(repo, handler, pw, progressHelper, getCancelSupport(handler, true));
+			FilteringInspector filterInsp = new FilteringInspector();
+			filterInsp.changesets(startRev, lastCset);
 			if (file == null) {
 				progressHelper.start(endRev - startRev + 1);
-				repo.getChangelog().range(startRev, endRev, this);
+				repo.getChangelog().range(startRev, endRev, filterInsp);
 				csetTransform.checkFailure();
 			} else {
-				progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/);
-				HgDataFile fileNode = repo.getFileNode(file);
-				if (!fileNode.exists()) {
-					throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file);
-				}
-				// FIXME startRev and endRev ARE CHANGESET REVISIONS, not that of FILE!!!
-				fileNode.history(startRev, endRev, this);
-				csetTransform.checkFailure();
 				final HgFileRenameHandlerMixin withCopyHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
-				if (fileNode.isCopy()) {
-					// even if we do not follow history, report file rename
-					do {
-						if (withCopyHandler != null) {
-							HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), null, fileNode.getCopySourceName());
-							HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), null, fileNode.getPath(), src.getPath());
-							withCopyHandler.copy(src, dst);
+				List<Pair<HgDataFile, Nodeid>> fileRenames = buildFileRenamesQueue();
+				progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/);
+
+				for (int nameIndex = 0, fileRenamesSize = fileRenames.size(); nameIndex < fileRenamesSize; nameIndex++) {
+					Pair<HgDataFile, Nodeid> curRename = fileRenames.get(nameIndex);
+					HgDataFile fileNode = curRename.first();
+					if (followAncestry) {
+						TreeBuildInspector treeBuilder = new TreeBuildInspector(followAncestry);
+						List<HistoryNode> fileAncestry = treeBuilder.go(fileNode, curRename.second());
+						int[] commitRevisions = narrowChangesetRange(treeBuilder.getCommitRevisions(), startRev, lastCset);
+						if (iterateDirection == IterateDirection.FromOldToNew) {
+							repo.getChangelog().range(filterInsp, commitRevisions);
+						} else {
+							assert iterateDirection == IterateDirection.FromNewToOld;
+							// visit one by one in the opposite direction
+							for (int i = commitRevisions.length-1; i >= 0; i--) {
+								int csetWithFileChange = commitRevisions[i];
+								repo.getChangelog().range(csetWithFileChange, csetWithFileChange, filterInsp);
+							}
 						}
-						if (limit > 0 && count >= limit) {
-							// if limit reach, follow is useless.
-							break;
+					} else {
+						// report complete file history (XXX may narrow range with [startRev, endRev], but need to go from file rev to link rev)
+						int fileStartRev = 0; //fileNode.getChangesetRevisionIndex(0) >= startRev
+						int fileEndRev = fileNode.getLastRevision();
+						fileNode.history(fileStartRev, fileEndRev, filterInsp);
+						csetTransform.checkFailure();
+					}
+					if (followRenames && withCopyHandler != null && nameIndex + 1 < fileRenamesSize) {
+						Pair<HgDataFile, Nodeid> nextRename = fileRenames.get(nameIndex+1);
+						HgFileRevision src, dst;
+						// A -> B
+						if (iterateDirection == IterateDirection.FromOldToNew) {
+							// curRename: A, nextRename: B
+							src = new HgFileRevision(fileNode, curRename.second(), null);
+							dst = new HgFileRevision(nextRename.first(), nextRename.first().getRevision(0), src.getPath());
+						} else {
+							assert iterateDirection == IterateDirection.FromNewToOld;
+							// curRename: B, nextRename: A
+							src = new HgFileRevision(nextRename.first(), nextRename.second(), null);
+							dst = new HgFileRevision(fileNode, fileNode.getRevision(0), src.getPath());
 						}
-						if (followRenames) {
-							fileNode = repo.getFileNode(fileNode.getCopySourceName());
-							fileNode.history(this);
-							csetTransform.checkFailure();
-						}
-					} while (followRenames && fileNode.isCopy());
-				}
-			}
+						withCopyHandler.copy(src, dst);
+					}
+				} // for renames
+			} // file != null
 		} catch (HgRuntimeException ex) {
 			throw new HgLibraryFailureException(ex);
 		} finally {
@@ -325,6 +354,43 @@
 		}
 	}
 	
+//	public static void main(String[] args) {
+//		int[] r = new int[] {17, 19, 21, 23, 25, 29};
+//		System.out.println(Arrays.toString(narrowChangesetRange(r, 0, 45)));
+//		System.out.println(Arrays.toString(narrowChangesetRange(r, 0, 25)));
+//		System.out.println(Arrays.toString(narrowChangesetRange(r, 5, 26)));
+//		System.out.println(Arrays.toString(narrowChangesetRange(r, 20, 26)));
+//		System.out.println(Arrays.toString(narrowChangesetRange(r, 26, 28)));
+//	}
+
+	private static int[] narrowChangesetRange(int[] csetRange, int startCset, int endCset) {
+		int lastInRange = csetRange[csetRange.length-1];
+		assert csetRange.length < 2 || csetRange[0] < lastInRange; // sorted
+		assert startCset >= 0 && startCset <= endCset;
+		if (csetRange[0] >= startCset && lastInRange <= endCset) {
+			// completely fits in
+			return csetRange;
+		}
+		if (csetRange[0] > endCset || lastInRange < startCset) {
+			return new int[0]; // trivial
+		}
+		int i = 0;
+		while (i < csetRange.length && csetRange[i] < startCset) {
+			i++;
+		}
+		int j = csetRange.length - 1;
+		while (j > i && csetRange[j] > endCset) {
+			j--;
+		}
+		if (i == j) {
+			// no values in csetRange fit into [startCset, endCset]
+			return new int[0];
+		}
+		int[] rv = new int[j-i+1];
+		System.arraycopy(csetRange, i, rv, 0, rv.length);
+		return rv;
+	}
+	
 	/**
 	 * Tree-wise iteration of a file history, with handy access to parent-child relations between changesets.
 	 * When file history is being followed, handler may additionally implement {@link HgFileRenameHandlerMixin} 
@@ -423,10 +489,13 @@
 	 * 
 	 * @return list of file renames, ordered with respect to {@link #iterateDirection}
 	 */
-	private List<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() {
+	private List<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() throws HgPathNotFoundException {
 		LinkedList<Pair<HgDataFile, Nodeid>> rv = new LinkedList<Pair<HgDataFile, Nodeid>>();
 		Nodeid startRev = null;
 		HgDataFile fileNode = repo.getFileNode(file);
+		if (!fileNode.exists()) {
+			throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file);
+		}
 		if (followAncestry) {
 			// TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex)
 			// or in the HgDataFile (getWorkingCopyOriginRevision)
@@ -751,31 +820,62 @@
 
 	//
 	
-	public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
-		if (limit > 0 && count >= limit) {
-			return;
-		}
-		if (branches != null && !branches.contains(cset.branch())) {
-			return;
+	private class FilteringInspector implements HgChangelog.Inspector, Lifecycle {
+	
+		private Callback lifecycle;
+		private int firstCset = BAD_REVISION, lastCset = BAD_REVISION;
+		
+		// limit to changesets in this range only
+		public void changesets(int start, int end) {
+			firstCset = start;
+			lastCset = end;
 		}
-		if (users != null) {
-			String csetUser = cset.user().toLowerCase();
-			boolean found = false;
-			for (String u : users) {
-				if (csetUser.indexOf(u) != -1) {
-					found = true;
-					break;
+
+		public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) {
+			if (limit > 0 && count >= limit) {
+				return;
+			}
+			// XXX may benefit from optional interface with #isInterested(int csetRev) - to avoid
+			// RawChangeset instantiation
+			if (firstCset != BAD_REVISION && revisionNumber < firstCset) {
+				return;
+			}
+			if (lastCset != BAD_REVISION && revisionNumber > lastCset) {
+				return;
+			}
+			if (branches != null && !branches.contains(cset.branch())) {
+				return;
+			}
+			if (users != null) {
+				String csetUser = cset.user().toLowerCase();
+				boolean found = false;
+				for (String u : users) {
+					if (csetUser.indexOf(u) != -1) {
+						found = true;
+						break;
+					}
+				}
+				if (!found) {
+					return;
 				}
 			}
-			if (!found) {
-				return;
+			if (date != null) {
+				// TODO post-1.0 implement date support for log
+			}
+			csetTransform.next(revisionNumber, nodeid, cset);
+			if (++count >= limit) {
+				if (lifecycle != null) { // FIXME support Lifecycle delegation
+				lifecycle.stop();
+				}
 			}
 		}
-		if (date != null) {
-			// TODO post-1.0 implement date support for log
+
+		public void start(int count, Callback callback, Object token) {
+			lifecycle = callback;
 		}
-		count++;
-		csetTransform.next(revisionNumber, nodeid, cset);
+
+		public void finish(Object token) {
+		}
 	}
 	
 	private HgParentChildMap<HgChangelog> getParentHelper(boolean create) throws HgInvalidControlFileException {
--- a/test/org/tmatesoft/hg/test/TestHistory.java	Tue Dec 18 19:08:00 2012 +0100
+++ b/test/org/tmatesoft/hg/test/TestHistory.java	Thu Dec 20 19:55:45 2012 +0100
@@ -30,6 +30,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
 import org.tmatesoft.hg.core.HgCallbackTargetException;
@@ -102,19 +103,14 @@
 		changelogParser.reset();
 		eh.run("hg", "log", "--debug", "--follow", f.toString());
 		
-		class H extends CollectHandler implements HgChangesetHandler.WithCopyHistory {
-			boolean copyReported = false;
-			boolean fromMatched = false;
-			public void copy(HgFileRevision from, HgFileRevision to) {
-				copyReported = true;
-				fromMatched = "src/com/tmate/hgkit/console/Remote.java".equals(from.getPath().toString());
-			}
-		};
-		H h = new H();
+		CollectWithRenameHandler h = new CollectWithRenameHandler();
 		new HgLogCommand(repo).file(f, true).execute(h);
+		errorCollector.assertEquals(1, h.renames.size());
+		HgFileRevision from = h.renames.get(0).first();
+		boolean fromMatched = "src/com/tmate/hgkit/console/Remote.java".equals(from.getPath().toString());
 		String what = "hg log - FOLLOW FILE HISTORY";
 		errorCollector.checkThat(what + "#copyReported ", h.copyReported, is(true));
-		errorCollector.checkThat(what + "#copyFromMatched", h.fromMatched, is(true));
+		errorCollector.checkThat(what + "#copyFromMatched", fromMatched, is(true));
 		//
 		// cmdline always gives in changesets in order from newest (bigger rev number) to oldest.
 		// LogCommand does other way round, from oldest to newest, follewed by revisions of copy source, if any
@@ -142,7 +138,7 @@
 	}
 	
 	@Test
-	public void testChangesetTreeFollowRename() throws Exception {
+	public void testChangesetTreeFollowRenameAndAncestry() throws Exception {
 		repo = Configuration.get().find("log-follow");
 		final String fname = "file1_b";
 		assertTrue("[sanity]", repo.getFileNode(fname).exists());
@@ -166,6 +162,103 @@
 		assertEquals(1, renames.size());
 		assertEquals(Path.create(fname), renames.get(Path.create("file1_a")));
 	}
+	
+	/**
+	 * Few tests  to check newly introduced followAncestry parameter to HgLogCommand:
+	 * followRename: true,	followAncestry: false
+	 * followRename: false,	followAncestry: true
+	 * followRename: true,	followAncestry: true
+	 * Perhaps, shall be merged with {@link #testFollowHistory()}
+	 */
+	@Test
+	public void testFollowRenamesNotAncestry() throws Exception {
+		repo = Configuration.get().find("log-follow");
+		final String fname1 = "file1_a";
+		final String fname2 = "file1_b";
+		assertTrue("[sanity]", repo.getFileNode(fname2).exists());
+		// no --follow, but two names we know have been the same file (fname1 renamed to fname2)
+		// sequentially gives follow rename semantics without ancestry
+		eh.run("hg", "log", "--debug", fname2, fname1, "--cwd", repo.getLocation());
+		
+		CollectWithRenameHandler h = new CollectWithRenameHandler();
+		new HgLogCommand(repo).file(fname2, true, false).execute(h);
+		errorCollector.assertEquals(1, h.renames.size());
+		Pair<HgFileRevision, HgFileRevision> rename = h.renames.get(0);
+		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
+		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
+		// Ensure rename info came in the right moment
+		errorCollector.assertEquals(1, h.lastChangesetReportedAtRename.size());
+		// Command iterates old to new, rename comes after last fname1 revision. Since we don't follow
+		// ancestry, it's the very last revision in fname1 history
+		String lastRevOfFname1 = "369c0882d477c11424a62eb4b791e86d1d4b6769";
+		errorCollector.assertEquals(lastRevOfFname1, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
+		report("HgChangesetHandler(renames: true, ancestry:false)", h.getChanges(), true);
+		
+		// TODO direction
+		// TODO TreeChangeHandler
+	}
+		
+	@Test
+	public void testFollowAncestryNotRenames() throws Exception {
+		repo = Configuration.get().find("log-follow");
+		final String fname2 = "file1_b";
+		assertTrue("[sanity]", repo.getFileNode(fname2).exists());
+		// to get "followed" history of fname2 only (without fname1 origin),
+		// get the complete history and keep there only elements that match fname2 own history 
+		eh.run("hg", "log", "--debug", "--follow", fname2, "--cwd", repo.getLocation());
+		final List<Record> fname2Follow = new LinkedList<LogOutputParser.Record>(changelogParser.getResult());
+		changelogParser.reset();
+		eh.run("hg", "log", "--debug", fname2, "--cwd", repo.getLocation());
+		// fname2Follow.retainAll(changelogParser.getResult());
+		for (Iterator<Record> it = fname2Follow.iterator(); it.hasNext();) {
+			Record r = it.next();
+			boolean belongsToSoleFname2History = false;
+			for (Record d : changelogParser.getResult()) {
+				if (d.changesetIndex == r.changesetIndex) {
+					assert d.changesetNodeid.equals(r.changesetNodeid) : "[sanity]";
+					belongsToSoleFname2History = true;
+					break;
+				}
+			}
+			if (!belongsToSoleFname2History) {
+				it.remove();
+			}
+		}
+		CollectWithRenameHandler h = new CollectWithRenameHandler();
+		new HgLogCommand(repo).file(fname2, false, true).execute(h);
+		errorCollector.assertEquals(0, h.renames.size());
+		report("HgChangesetHandler(renames: false, ancestry:true)", h.getChanges(), fname2Follow, true, errorCollector);
+
+		// TODO direction
+		// TODO TreeChangeHandler
+	}
+
+	/**
+	 * output identical to that of "hg log --follow"
+	 */
+	@Test
+	public void testFollowBothRenameAndAncestry() throws Exception {
+		repo = Configuration.get().find("log-follow");
+		final String fname1 = "file1_a";
+		final String fname2 = "file1_b";
+		assertTrue("[sanity]", repo.getFileNode(fname2).exists());
+		eh.run("hg", "log", "--debug", "--follow", fname2, "--cwd", repo.getLocation());
+		
+		CollectWithRenameHandler h = new CollectWithRenameHandler();
+		new HgLogCommand(repo).file(fname2, true, true).execute(h);
+		errorCollector.assertEquals(1, h.renames.size());
+		Pair<HgFileRevision, HgFileRevision> rename = h.renames.get(0);
+		errorCollector.assertEquals(fname1, rename.first().getPath().toString());
+		errorCollector.assertEquals(fname2, rename.second().getPath().toString());
+		// Ensure rename info came in the right moment
+		errorCollector.assertEquals(1, h.lastChangesetReportedAtRename.size());
+		String fname1BranchRevision = "6e668ff2940acb250c8627843f8116166fe5d5cd";
+		errorCollector.assertEquals(fname1BranchRevision, h.lastChangesetReportedAtRename.get(0).getNodeid().toString());
+		// finally, match output
+		report("HgChangesetHandler(renames: true, ancestry:true)", h.getChanges(), true);
+		// TODO direction
+		// TreeChangeHandler in #testChangesetTreeFollowRenameAndAncestry
+	}
 
 	private void report(String what, List<HgChangeset> r, boolean reverseConsoleResult) {
 		final List<Record> consoleResult = changelogParser.getResult();
@@ -177,7 +270,7 @@
 		if (reverseConsoleResult) {
 			Collections.reverse(consoleResult);
 		}
-		errorCollector.checkThat(what + ". Number of changeset reported didn't match", consoleResult.size(), equalTo(hg4jResult.size()));
+		errorCollector.checkThat(what + ". Number of changeset reported didn't match", hg4jResult.size(), equalTo(consoleResult.size()));
 		Iterator<Record> consoleResultItr = consoleResult.iterator();
 		for (HgChangeset cs : hg4jResult) {
 			if (!consoleResultItr.hasNext()) {
@@ -235,7 +328,7 @@
 		//
 		changelogParser.reset();
 		eh.run("hg", "log", "--debug", "-f", "e", "--cwd", repo.getLocation());
-		report("log -f e", cmd.file("e", true).execute(), false /*#1, below*/);
+		report("log -f e", cmd.file("e", true).execute(), true);
 		//
 		changelogParser.reset();
 		eh.run("hg", "log", "--debug", "dir/b", "--cwd", repo.getLocation());
@@ -261,7 +354,7 @@
 		report("log -f a", r, true);
 		changelogParser.reset();
 		eh.run("hg", "log", "--debug", "-f", "dir/b", "--cwd", repo.getLocation());
-		report("log -f dir/b", cmd.file("dir/b", true).execute(), false /*#1, below*/);
+		report("log -f dir/b", cmd.file("dir/b", true).execute(), true);
 		//
 		// get repo back into clear state, up to the tip
 		eh.run("hg", "update", "-q", "--cwd", repo.getLocation());
@@ -383,7 +476,19 @@
 				cmdResult.addLast(entry.changeset());
 			}
 		}
-		
-		
 	}
+
+	private static class CollectWithRenameHandler extends CollectHandler implements HgChangesetHandler.WithCopyHistory {
+		public boolean copyReported = false;
+		public List<Pair<HgFileRevision, HgFileRevision>> renames = new LinkedList<Pair<HgFileRevision,HgFileRevision>>();
+		public List<HgChangeset> lastChangesetReportedAtRename = new LinkedList<HgChangeset>(); 
+
+		public void copy(HgFileRevision from, HgFileRevision to) {
+			copyReported = true;
+			Assert.assertTrue("Renames couldn't be reported prior to any change", getChanges().size() > 0);
+			HgChangeset lastKnown = getChanges().get(getChanges().size() - 1);
+			lastChangesetReportedAtRename.add(lastKnown);
+			renames.add(new Pair<HgFileRevision, HgFileRevision>(from, to));
+		}
+	};
 }