# HG changeset patch # User Artem Tikhomirov # Date 1295924072 -3600 # Node ID 4222b04f34ee885bc1ad547c7ef330e18a51afc1 # Parent 5f9635c016819b322ae05a91b3378621b538c933 Follow history of a file diff -r 5f9635c01681 -r 4222b04f34ee cmdline/org/tmatesoft/hg/console/Log.java --- 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); diff -r 5f9635c01681 -r 4222b04f34ee src/org/tmatesoft/hg/core/LogCommand.java --- 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 null 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 result = new LinkedList(); @@ -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); } } } diff -r 5f9635c01681 -r 4222b04f34ee src/org/tmatesoft/hg/internal/RevlogStream.java --- 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; } diff -r 5f9635c01681 -r 4222b04f34ee src/org/tmatesoft/hg/internal/StoragePathHelper.java --- 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. diff -r 5f9635c01681 -r 4222b04f34ee src/org/tmatesoft/hg/repo/Revlog.java --- 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;