changeset 80:4222b04f34ee

Follow history of a file
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 25 Jan 2011 03:54:32 +0100
parents 5f9635c01681
children 40d04c4f771e 7255c971dd66
files cmdline/org/tmatesoft/hg/console/Log.java src/org/tmatesoft/hg/core/LogCommand.java src/org/tmatesoft/hg/internal/RevlogStream.java src/org/tmatesoft/hg/internal/StoragePathHelper.java src/org/tmatesoft/hg/repo/Revlog.java
diffstat 5 files changed, 90 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/cmdline/org/tmatesoft/hg/console/Log.java	Tue Jan 25 02:26:06 2011 +0100
+++ b/cmdline/org/tmatesoft/hg/console/Log.java	Tue Jan 25 03:54:32 2011 +0100
@@ -84,14 +84,14 @@
 				System.out.println("History of the file: " + f1.getPath());
 				String normalizesName = hgRepo.getPathHelper().rewrite(fname);
 				if (cmdLineOpts.limit == -1) {
-					cmd.file(Path.create(normalizesName)).execute(dump);
+					cmd.file(Path.create(normalizesName), true).execute(dump);
 				} else {
 					int[] r = new int[] { 0, f1.getRevisionCount() };
 					if (fixRange(r, dump.reverseOrder, cmdLineOpts.limit) == 0) {
 						System.out.println("No changes");
 						continue;
 					}
-					cmd.range(r[0], r[1]).file(Path.create(normalizesName)).execute(dump);
+					cmd.range(r[0], r[1]).file(Path.create(normalizesName), true).execute(dump);
 				}
 				dump.complete();
 			}
@@ -115,7 +115,7 @@
 		return rv;
 	}
 
-	private static final class Dump implements LogCommand.Handler {
+	private static final class Dump implements LogCommand.FileHistoryHandler {
 		// params
 		boolean complete = false; // roughly --debug
 		boolean reverseOrder = false;
@@ -130,10 +130,17 @@
 			repo = hgRepo;
 			tip = hgRepo.getChangelog().getRevisionCount() - 1;
 		}
+		
+		public void copy(FileRevision from, FileRevision to) {
+			System.out.printf("Got notified that %s(%s) was originally known as %s(%s)\n", to.getPath(), to.getRevision(), from.getPath(), from.getRevision());
+		}
 
 		public void next(Cset changeset) {
 			final String s = print(changeset);
 			if (reverseOrder) {
+				// XXX in fact, need to insert s into l according to changeset.getRevision()
+				// because when file history is being followed, revisions of the original file (with smaller revNumber)
+				// are reported *after* revisions of present file and with addFirst appear above them
 				l.addFirst(s);
 			} else {
 				System.out.print(s);
--- a/src/org/tmatesoft/hg/core/LogCommand.java	Tue Jan 25 02:26:06 2011 +0100
+++ b/src/org/tmatesoft/hg/core/LogCommand.java	Tue Jan 25 03:54:32 2011 +0100
@@ -27,6 +27,7 @@
 import java.util.TreeSet;
 
 import org.tmatesoft.hg.repo.Changeset;
+import org.tmatesoft.hg.repo.HgDataFile;
 import org.tmatesoft.hg.repo.HgRepository;
 import org.tmatesoft.hg.repo.StatusCollector;
 import org.tmatesoft.hg.util.PathPool;
@@ -51,8 +52,9 @@
 	private Handler delegate;
 	private Calendar date;
 	private Path file;
+	private boolean followHistory; // makes sense only when file != null
 	private Cset changeset;
-
+	
 	public LogCommand(HgRepository hgRepo) {
 		this.repo = hgRepo;
 	}
@@ -133,11 +135,12 @@
 	/**
 	 * Visit history of a given file only.
 	 * @param file path relative to repository root. Pass <code>null</code> to reset.
+	 * @param followCopyRename true to report changesets of the original file(-s), if copy/rename ever occured to the file. 
 	 */
-	public LogCommand file(Path file) {
+	public LogCommand file(Path file, boolean followCopyRename) {
 		// multiple? Bad idea, would need to include extra method into Handler to tell start of next file
-		// implicit --follow in this case
 		this.file = file;
+		followHistory = followCopyRename;
 		return this;
 	}
 
@@ -170,7 +173,24 @@
 			if (file == null) {
 				repo.getChangelog().range(startRev, endRev, this);
 			} else {
-				repo.getFileNode(file).history(startRev, endRev, this);
+				HgDataFile fileNode = repo.getFileNode(file);
+				fileNode.history(startRev, endRev, this);
+				if (handler instanceof FileHistoryHandler && fileNode.isCopy()) {
+					// even if we do not follow history, report file rename
+					do {
+						FileRevision src = new FileRevision(repo, fileNode.getCopySourceRevision(), fileNode.getCopySourceName());
+						FileRevision dst = new FileRevision(repo, fileNode.getRevisionNumber(0), fileNode.getPath());
+						((FileHistoryHandler) handler).copy(src, dst);
+						if (limit > 0 && count >= limit) {
+							// if limit reach, follow is useless.
+							break;
+						}
+						if (followHistory) {
+							fileNode = repo.getFileNode(src.getPath());
+							fileNode.history(this);
+						}
+					} while (followHistory && fileNode.isCopy());
+				}
 			}
 		} finally {
 			delegate = null;
@@ -215,6 +235,23 @@
 		void next(Cset changeset);
 	}
 	
+	/**
+	 * When {@link LogCommand} is executed against file, handler passed to {@link LogCommand#execute(Handler)} may optionally
+	 * implement this interface to get information about file renames. Method {@link #copy(FileRevision, FileRevision)} would
+	 * get invoked prior any changeset of the original file (if file history being followed) is reported via {@link #next(Cset)}.
+	 * 
+	 * For {@link LogCommand#file(Path, boolean)} with renamed file path and follow argument set to false, 
+	 * {@link #copy(FileRevision, FileRevision)} would be invoked for the first copy/rename in the history of the file, but not 
+	 * followed by any changesets. 
+	 *
+	 * @author Artem Tikhomirov
+	 * @author TMate Software Ltd.
+	 */
+	public interface FileHistoryHandler extends Handler {
+		// XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them?
+		void copy(FileRevision from, FileRevision to);
+	}
+	
 	public static class CollectHandler implements Handler {
 		private final List<Cset> result = new LinkedList<Cset>();
 
@@ -232,7 +269,7 @@
 		private final Nodeid revision;
 		private final Path path;
 		
-		public FileRevision(HgRepository hgRepo, Nodeid rev, Path p) {
+		/*package-local*/FileRevision(HgRepository hgRepo, Nodeid rev, Path p) {
 			if (hgRepo == null || rev == null || p == null) {
 				throw new IllegalArgumentException();
 			}
@@ -249,7 +286,7 @@
 		}
 		public byte[] getContent() {
 			// XXX Content wrapper, to allow formats other than byte[], e.g. Stream, DataAccess, etc?
-			return repo.getFileNode(path).content();
+			return repo.getFileNode(path).content(revision);
 		}
 	}
 }
--- a/src/org/tmatesoft/hg/internal/RevlogStream.java	Tue Jan 25 02:26:06 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/RevlogStream.java	Tue Jan 25 03:54:32 2011 +0100
@@ -16,6 +16,7 @@
  */
 package org.tmatesoft.hg.internal;
 
+import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import java.io.File;
@@ -28,6 +29,7 @@
 import java.util.zip.Inflater;
 
 import org.tmatesoft.hg.core.Nodeid;
+import org.tmatesoft.hg.repo.HgRepository;
 
 
 /**
@@ -89,11 +91,35 @@
 		}
 	}
 	
+	public byte[] nodeid(int revision) {
+		final int indexSize = revisionCount();
+		if (revision == TIP) {
+			revision = indexSize - 1;
+		}
+		if (revision < 0 || revision >= indexSize) {
+			throw new IllegalArgumentException(Integer.toString(revision));
+		}
+		DataAccess daIndex = getIndexStream();
+		try {
+			int recordOffset = inline ? (int) index.get(revision).offset : revision * REVLOGV1_RECORD_SIZE;
+			daIndex.seek(recordOffset + 32);
+			byte[] rv = new byte[20];
+			daIndex.readBytes(rv, 0, 20);
+			return rv;
+		} catch (IOException ex) {
+			ex.printStackTrace();
+			throw new IllegalStateException();
+		} finally {
+		}
+	}
+	
 	// Perhaps, RevlogStream should be limited to use of plain int revisions for access,
 	// while Nodeids should be kept on the level up, in Revlog. Guess, Revlog better keep
 	// map of nodeids, and once this comes true, we may get rid of this method.
-	// Unlike its counterpart, Revlog#getLocalRevisionNumber, doesn't fail with exception if node not found,
-	// returns a predefined constant instead
+	// Unlike its counterpart, {@link Revlog#getLocalRevisionNumber()}, doesn't fail with exception if node not found,
+	/**
+	 * @return integer in [0..revisionCount()) or {@link HgRepository#BAD_REVISION} if not found
+	 */
 	public int findLocalRevisionNumber(Nodeid nodeid) {
 		// XXX this one may be implemented with iterate() once there's mechanism to stop iterations
 		final int indexSize = revisionCount();
@@ -116,7 +142,7 @@
 		} finally {
 			daIndex.done();
 		}
-		return Integer.MIN_VALUE;
+		return BAD_REVISION;
 	}
 
 
--- a/src/org/tmatesoft/hg/internal/StoragePathHelper.java	Tue Jan 25 02:26:06 2011 +0100
+++ b/src/org/tmatesoft/hg/internal/StoragePathHelper.java	Tue Jan 25 03:54:32 2011 +0100
@@ -24,6 +24,7 @@
 
 /**
  * @see http://mercurial.selenic.com/wiki/CaseFoldingPlan
+ * @see http://mercurial.selenic.com/wiki/fncacheRepoFormat
  * 
  * @author Artem Tikhomirov
  * @author TMate Software Ltd.
--- a/src/org/tmatesoft/hg/repo/Revlog.java	Tue Jan 25 02:26:06 2011 +0100
+++ b/src/org/tmatesoft/hg/repo/Revlog.java	Tue Jan 25 03:54:32 2011 +0100
@@ -16,6 +16,7 @@
  */
 package org.tmatesoft.hg.repo;
 
+import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION;
 import static org.tmatesoft.hg.repo.HgRepository.TIP;
 
 import java.util.Arrays;
@@ -58,10 +59,15 @@
 	public int getRevisionCount() {
 		return content.revisionCount();
 	}
+	
+	public Nodeid getRevisionNumber(int revision) {
+		// XXX cache nodeids?
+		return Nodeid.fromBinary(content.nodeid(revision), 0);
+	}
 
 	public int getLocalRevisionNumber(Nodeid nid) {
 		int revision = content.findLocalRevisionNumber(nid);
-		if (revision == Integer.MIN_VALUE) {
+		if (revision == BAD_REVISION) {
 			throw new IllegalArgumentException(String.format("%s doesn't represent a revision of %s", nid.toString(), this /*XXX HgDataFile.getPath might be more suitable here*/));
 		}
 		return revision;