# HG changeset patch # User Artem Tikhomirov # Date 1356029745 -3600 # Node ID 0d5e1ea7955e8ddfc1fe7b3e1948787a0a06044a # Parent 9922d1f7cb2addb337106e13fc0e2eae75f153d2 Tests for HgLogCommand#execute(HgChangesetHandler) with various combination of follow renames and ancestry diff -r 9922d1f7cb2a -r 0d5e1ea7955e src/org/tmatesoft/hg/core/HgFileRevision.java --- 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); } diff -r 9922d1f7cb2a -r 0d5e1ea7955e src/org/tmatesoft/hg/core/HgLogCommand.java --- 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 implements HgChangelog.Inspector { +public class HgLogCommand extends HgAbstractCommand { private final HgRepository repo; private Set 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> 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 curRename = fileRenames.get(nameIndex); + HgDataFile fileNode = curRename.first(); + if (followAncestry) { + TreeBuildInspector treeBuilder = new TreeBuildInspector(followAncestry); + List 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 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> buildFileRenamesQueue() { + private List> buildFileRenamesQueue() throws HgPathNotFoundException { LinkedList> rv = new LinkedList>(); 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 getParentHelper(boolean create) throws HgInvalidControlFileException { diff -r 9922d1f7cb2a -r 0d5e1ea7955e test/org/tmatesoft/hg/test/TestHistory.java --- 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 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 fname2Follow = new LinkedList(changelogParser.getResult()); + changelogParser.reset(); + eh.run("hg", "log", "--debug", fname2, "--cwd", repo.getLocation()); + // fname2Follow.retainAll(changelogParser.getResult()); + for (Iterator 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 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 r, boolean reverseConsoleResult) { final List 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 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> renames = new LinkedList>(); + public List lastChangesetReportedAtRename = new LinkedList(); + + 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(from, to)); + } + }; }