diff src/org/tmatesoft/hg/core/HgLogCommand.java @ 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 1ee452f31187
line wrap: on
line diff
--- 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 {