# HG changeset patch # User Artem Tikhomirov # Date 1356121226 -3600 # Node ID 1ee452f31187f262cf589623fea799006c94aeae # Parent 934037edbea0593ac7ac87361bec0eb94fdd5d85 Experimental support for inverse direction history walking. Refactored/streamlined cancellation in HgLogCommand and down the stack diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/core/ChangesetTransformer.java --- a/src/org/tmatesoft/hg/core/ChangesetTransformer.java Thu Dec 20 20:21:59 2012 +0100 +++ b/src/org/tmatesoft/hg/core/ChangesetTransformer.java Fri Dec 21 21:20:26 2012 +0100 @@ -18,6 +18,8 @@ import java.util.Set; +import org.tmatesoft.hg.internal.Lifecycle; +import org.tmatesoft.hg.internal.LifecycleBridge; import org.tmatesoft.hg.internal.PathPool; import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; @@ -37,14 +39,12 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -/*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector, Adaptable, CancelSupport { +/*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector, Adaptable { private final HgChangesetHandler handler; - private final ProgressSupport progressHelper; - private final CancelSupport cancelHelper; + private final LifecycleBridge lifecycleBridge; private final Transformation t; private Set branches; private HgCallbackTargetException failure; - private CancelledException cancellation; // repo and delegate can't be null, parent walker can // ps and cs can't be null @@ -58,11 +58,9 @@ HgStatusCollector statusCollector = new HgStatusCollector(hgRepo); t = new Transformation(statusCollector, pw); handler = delegate; - // we let HgChangelog#range deal with progress (pipe through getAdapter) - // but use own cancellation (which involves CallbackTargetException as well, and preserves original cancellation - // exception in case clients care) - cancelHelper = cs; - progressHelper = ps; + // lifecycleBridge takes care of progress and cancellation, plus + // gives us explicit way to stop iteration (once HgCallbackTargetException) comes. + lifecycleBridge = new LifecycleBridge(ps, cs); } public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { @@ -73,23 +71,21 @@ HgChangeset changeset = t.handle(revisionNumber, nodeid, cset); try { handler.cset(changeset); - cancelHelper.checkCancelled(); + lifecycleBridge.nextStep(); } catch (HgCallbackTargetException ex) { failure = ex.setRevision(nodeid).setRevisionIndex(revisionNumber); - } catch (CancelledException ex) { - cancellation = ex; + lifecycleBridge.stop(); } } public void checkFailure() throws HgCallbackTargetException, CancelledException { if (failure != null) { HgCallbackTargetException toThrow = failure; - failure = null; // just in (otherwise unexpected) case this instance would get reused throw toThrow; } - if (cancellation != null) { - CancelledException toThrow = cancellation; - cancellation = null; + if (lifecycleBridge.isCancelled()) { + CancelledException toThrow = lifecycleBridge.getCancelOrigin(); + assert toThrow != null; throw toThrow; } } @@ -121,18 +117,11 @@ } } - public void checkCancelled() throws CancelledException { - if (failure != null || cancellation != null) { - // stop HgChangelog.Iterator. Our exception is for the purposes of cancellation only, - // the one we have stored (this.cancellation) is for user - throw new CancelledException(); + public T getAdapter(Class adapterClass) { + if (adapterClass == Lifecycle.class) { + return adapterClass.cast(lifecycleBridge); } - } - - public T getAdapter(Class adapterClass) { - if (adapterClass == ProgressSupport.class) { - return adapterClass.cast(progressHelper); - } - return null; + // just in case there are more adapters in future + return Adaptable.Factory.getAdapter(handler, adapterClass, null); } } \ No newline at end of file diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/core/HgLogCommand.java --- a/src/org/tmatesoft/hg/core/HgLogCommand.java Thu Dec 20 20:21:59 2012 +0100 +++ b/src/org/tmatesoft/hg/core/HgLogCommand.java Fri Dec 21 21:20:26 2012 +0100 @@ -34,9 +34,13 @@ import java.util.Set; import java.util.TreeSet; +import org.tmatesoft.hg.internal.AdapterPlug; +import org.tmatesoft.hg.internal.BatchRangeHelper; +import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.IntMap; import org.tmatesoft.hg.internal.IntVector; import org.tmatesoft.hg.internal.Lifecycle; +import org.tmatesoft.hg.internal.LifecycleProxy; import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgDataFile; @@ -287,19 +291,41 @@ throw new HgBadArgumentException(String.format("Bad value %d for start revision for range [%1$d..%d]", startRev, lastCset), null); } final ProgressSupport progressHelper = getProgressSupport(handler); + final int BATCH_SIZE = 100; try { count = 0; HgParentChildMap pw = getParentHelper(file == null); // leave it uninitialized unless we iterate whole repo // 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 is responsible to check command arguments: users, branches, limit, etc. + // prior to passing cset to next Inspector, which is either (a) collector to reverse cset order, then invokes + // transformer from (b), below, with alternative cset order or (b) transformer to hi-level csets. FilteringInspector filterInsp = new FilteringInspector(); filterInsp.changesets(startRev, lastCset); if (file == null) { - progressHelper.start(endRev - startRev + 1); - repo.getChangelog().range(startRev, endRev, filterInsp); - csetTransform.checkFailure(); + progressHelper.start(lastCset - startRev + 1); + if (iterateDirection == IterateDirection.FromOldToNew) { + filterInsp.delegateTo(csetTransform); + repo.getChangelog().range(startRev, lastCset, filterInsp); + csetTransform.checkFailure(); + } else { + assert iterateDirection == IterateDirection.FromNewToOld; + BatchRangeHelper brh = new BatchRangeHelper(startRev, lastCset, BATCH_SIZE, true); + BatchChangesetInspector batchInspector = new BatchChangesetInspector(Math.min(lastCset-startRev+1, BATCH_SIZE)); + filterInsp.delegateTo(batchInspector); + while (brh.hasNext()) { + brh.next(); + repo.getChangelog().range(brh.start(), brh.end(), filterInsp); + for (BatchChangesetInspector.BatchRecord br : batchInspector.iterate(true)) { + csetTransform.next(br.csetIndex, br.csetRevision, br.cset); + csetTransform.checkFailure(); + } + batchInspector.reset(); + } + } } else { + filterInsp.delegateTo(csetTransform); final HgFileRenameHandlerMixin withCopyHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null); List> fileRenames = buildFileRenamesQueue(); progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/); @@ -309,10 +335,12 @@ HgDataFile fileNode = curRename.first(); if (followAncestry) { TreeBuildInspector treeBuilder = new TreeBuildInspector(followAncestry); + @SuppressWarnings("unused") List fileAncestry = treeBuilder.go(fileNode, curRename.second()); int[] commitRevisions = narrowChangesetRange(treeBuilder.getCommitRevisions(), startRev, lastCset); if (iterateDirection == IterateDirection.FromOldToNew) { repo.getChangelog().range(filterInsp, commitRevisions); + csetTransform.checkFailure(); } else { assert iterateDirection == IterateDirection.FromNewToOld; // visit one by one in the opposite direction @@ -325,8 +353,24 @@ // 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 (iterateDirection == IterateDirection.FromOldToNew) { + fileNode.history(fileStartRev, fileEndRev, filterInsp); + csetTransform.checkFailure(); + } else { + assert iterateDirection == IterateDirection.FromNewToOld; + BatchRangeHelper brh = new BatchRangeHelper(fileStartRev, fileEndRev, BATCH_SIZE, true); + BatchChangesetInspector batchInspector = new BatchChangesetInspector(Math.min(fileEndRev-fileStartRev+1, BATCH_SIZE)); + filterInsp.delegateTo(batchInspector); + while (brh.hasNext()) { + brh.next(); + fileNode.history(brh.start(), brh.end(), filterInsp); + for (BatchChangesetInspector.BatchRecord br : batchInspector.iterate(true /*iterateDirection == IterateDirection.FromNewToOld*/)) { + csetTransform.next(br.csetIndex, br.csetRevision, br.cset); + csetTransform.checkFailure(); + } + batchInspector.reset(); + } + } } if (followRenames && withCopyHandler != null && nameIndex + 1 < fileRenamesSize) { Pair nextRename = fileRenames.get(nameIndex+1); @@ -354,6 +398,46 @@ } } + private static class BatchChangesetInspector extends AdapterPlug implements HgChangelog.Inspector { + private static class BatchRecord { + public final int csetIndex; + public final Nodeid csetRevision; + public final RawChangeset cset; + + public BatchRecord(int index, Nodeid nodeid, RawChangeset changeset) { + csetIndex = index; + csetRevision = nodeid; + cset = changeset; + } + } + private final ArrayList batch; + + public BatchChangesetInspector(int batchSizeHint) { + batch = new ArrayList(batchSizeHint); + } + + public BatchChangesetInspector reset() { + batch.clear(); + return this; + } + + public void next(int revisionIndex, Nodeid nodeid, RawChangeset cset) { + batch.add(new BatchRecord(revisionIndex, nodeid, cset.clone())); + } + + public Iterable iterate(final boolean reverse) { + return new Iterable() { + + public Iterator iterator() { + return reverse ? new ReverseIterator(batch) : batch.iterator(); + } + }; + } + + // alternative would be dispatch(HgChangelog.Inspector) and dispatchReverse() + // methods, but progress and cancellation might get messy then + } + // 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))); @@ -454,6 +538,21 @@ progressHelper.done(); } + /** + * DO NOT USE THIS METHOD, DEBUG PURPOSES ONLY!!! + */ + @Experimental(reason="Work in progress") + public HgLogCommand debugSwitch1() { + // FIXME can't expose iteration direction unless general iteration (changelog, not a file) supports it, too. + // however, need to test the code already there, hence this debug switch + if (iterateDirection == IterateDirection.FromOldToNew) { + iterateDirection = IterateDirection.FromNewToOld; + } else { + iterateDirection = IterateDirection.FromOldToNew; + } + return this; + } + private IterateDirection iterateDirection = IterateDirection.FromOldToNew; private static class ReverseIterator implements Iterator { @@ -820,16 +919,29 @@ // - private class FilteringInspector implements HgChangelog.Inspector, Lifecycle { + private class FilteringInspector extends AdapterPlug implements HgChangelog.Inspector, Adaptable { - private Callback lifecycle; private int firstCset = BAD_REVISION, lastCset = BAD_REVISION; + private HgChangelog.Inspector delegate; + // we use lifecycle to stop when limit is reached. + // delegate, however, may use lifecycle, too, so give it a chance + private LifecycleProxy lifecycleProxy; // limit to changesets in this range only public void changesets(int start, int end) { firstCset = start; lastCset = end; } + + public void delegateTo(HgChangelog.Inspector inspector) { + delegate = inspector; + // let delegate control life cycle, too + if (lifecycleProxy == null) { + super.attachAdapter(Lifecycle.class, lifecycleProxy = new LifecycleProxy(inspector)); + } else { + lifecycleProxy.init(inspector); + } + } public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { if (limit > 0 && count >= limit) { @@ -862,22 +974,14 @@ 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(); - } + delegate.next(revisionNumber, nodeid, cset); + count++; + if (limit > 0 && count >= limit) { + lifecycleProxy.stop(); } } + } - public void start(int count, Callback callback, Object token) { - lifecycle = callback; - } - - public void finish(Object token) { - } - } - private HgParentChildMap getParentHelper(boolean create) throws HgInvalidControlFileException { if (parentHelper == null && create) { parentHelper = new HgParentChildMap(repo.getChangelog()); @@ -885,8 +989,7 @@ } return parentHelper; } - - + public static class CollectHandler implements HgChangesetHandler { private final List result = new LinkedList(); diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/internal/AdapterPlug.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/AdapterPlug.java Fri Dec 21 21:20:26 2012 +0100 @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.tmatesoft.hg.util.Adaptable; + +/** + * Implementation of {@link Adaptable} that allows to add ("plug") + * adapters as needed + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class AdapterPlug implements Adaptable { + private final Map, Object> adapters = new HashMap, Object>(); + private final List> adapterUses = new ArrayList>(); + + public void attachAdapter(Class adapterClass, T instance) { + adapters.put(adapterClass, instance); + } + + public T getAdapter(Class adapterClass) { + Object instance = adapters.get(adapterClass); + if (instance != null) { + adapterUses.add(adapterClass); + return adapterClass.cast(instance); + } + return null; + } + + public int getAdapterUse(Class adapterClass) { + int uses = 0; + for (Class c : adapterUses) { + if (c == adapterClass) { + uses++; + } + } + return uses; + } +} \ No newline at end of file diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/internal/BatchRangeHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/BatchRangeHelper.java Fri Dec 21 21:20:26 2012 +0100 @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2012 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import java.util.Arrays; +import java.util.NoSuchElementException; + +/** + * Helper to break given range [start..end] (inclusive bounds) to series of ranges, + * all but last are of batchSize length, and the last one is at most of batchSize+batchSizeTolerance length. + * + * Range is [{@link #start()rangeStart}..{@link #end() rangeEnd}], where rangeStart is less or equal to rangeEnd. + * + * When reverse range iteration is requested, original range is iterated from end to start, but the subranges + * boundaries are in natural order. i.e. for 0..100, 10 first subrange would be [91..100], not [100..91]. This + * helps clients of this class to get [start()..end()] in natural order regardless of iteration direction. + * + * Note, this class (and its treatment of inclusive boundaries) is designed solely for use with methods that navigate + * revlogs and take (start,end) pair of inclusive range. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class BatchRangeHelper { + + private final int rangeCount; + private final int rangeDelta; + private final int nextValueDelta; + private final int firstBoundary, lastBoundary; + private int rangeIndex, rangeValue, rangeStart, rangeEnd; + + public BatchRangeHelper(int start, int end, int batchSize, boolean reverse) { + this(start, end, batchSize, batchSize/5, reverse); + } + + public BatchRangeHelper(int start, int end, int batchSize, int batchSizeTolerance, boolean reverse) { + assert end >= start; + assert start >= 0; + assert batchSize > 0; + assert batchSizeTolerance >= 0; + final int totalElements = end-start+1; + int batchRangeCount = totalElements / batchSize; + // batchRangeCount == 0, totalElements > 0 => need at least 1 range + if (batchRangeCount == 0 || batchRangeCount*batchSize+batchSizeTolerance < totalElements) { + batchRangeCount++; + } + rangeCount = batchRangeCount; + rangeDelta = batchSize-1; // ranges are inclusive, and always grow naturally. + nextValueDelta = reverse ? -batchSize : batchSize; + firstBoundary = reverse ? end-rangeDelta : start; + lastBoundary = reverse ? start : end; + reset(); + } + + public boolean hasNext() { + return rangeIndex < rangeCount; + } + + public void next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + rangeStart = rangeValue; + rangeEnd = rangeValue + rangeDelta; + rangeValue += nextValueDelta; + if (++rangeIndex >= rangeCount) { + if (nextValueDelta < 0) { + // reverse iteration, lastBoundary represents start + rangeStart = lastBoundary; + } else { + // lastBoundary represents end + rangeEnd = lastBoundary; + } + } + } + + public int start() { + return rangeStart; + } + + public int end() { + return rangeEnd; + } + + public BatchRangeHelper reset() { + rangeValue = firstBoundary; + rangeIndex = 0; + return this; + } + + public int[] toArray() { + int[] rv = new int[rangeCount*2]; + reset(); + int i = 0; + while (hasNext()) { + next(); + rv[i++] = start(); + rv[i++] = end(); + } + reset(); + return rv; + } + + public static void main(String[] args) { + System.out.println("With remainder within tolerance"); + System.out.println(Arrays.toString(new BatchRangeHelper(0, 102, 10, 4, false).toArray())); + System.out.println(Arrays.toString(new BatchRangeHelper(0, 102, 10, 4, true).toArray())); + System.out.println("With remainder out of tolerance"); + System.out.println(Arrays.toString(new BatchRangeHelper(0, 102, 10, 2, false).toArray())); + System.out.println(Arrays.toString(new BatchRangeHelper(0, 102, 10, 2, true).toArray())); + System.out.println("Range smaller than batch"); + System.out.println(Arrays.toString(new BatchRangeHelper(1, 9, 10, false).toArray())); + System.out.println(Arrays.toString(new BatchRangeHelper(1, 9, 10, true).toArray())); + System.out.println("Range smaller than batch and smaller than tolerance"); + System.out.println(Arrays.toString(new BatchRangeHelper(1, 9, 10, 20, false).toArray())); + System.out.println(Arrays.toString(new BatchRangeHelper(1, 9, 10, 20, true).toArray())); + System.out.println("Zero tolerance"); + System.out.println(Arrays.toString(new BatchRangeHelper(0, 100, 10, 0, false).toArray())); + System.out.println(Arrays.toString(new BatchRangeHelper(0, 100, 10, 0, true).toArray())); + System.out.println("Right to boundary"); + System.out.println(Arrays.toString(new BatchRangeHelper(1, 100, 10, false).toArray())); + System.out.println(Arrays.toString(new BatchRangeHelper(1, 100, 10, true).toArray())); + } +} diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/internal/IterateControlMediator.java --- a/src/org/tmatesoft/hg/internal/IterateControlMediator.java Thu Dec 20 20:21:59 2012 +0100 +++ b/src/org/tmatesoft/hg/internal/IterateControlMediator.java Fri Dec 21 21:20:26 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 TMate Software Ltd * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -28,7 +28,8 @@ public class IterateControlMediator { private final CancelSupport src; - private Callback receiver; + private final Callback receiver; + private CancelledException cancellation; public IterateControlMediator(CancelSupport source, Lifecycle.Callback target) { assert target != null; @@ -40,11 +41,15 @@ if (src == null) { return false; } + if (cancellation != null) { + return true; + } try { src.checkCancelled(); return false; } catch (CancelledException ex) { receiver.stop(); + cancellation = ex; return true; } } @@ -52,4 +57,8 @@ public void stop() { receiver.stop(); } + + public CancelledException getCancelException() { + return cancellation; + } } diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/internal/LifecycleBridge.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/LifecycleBridge.java Fri Dec 21 21:20:26 2012 +0100 @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2012 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.ProgressSupport; + +/** + * Bridge low-level life-cycle ({@link Lifecycle}) API with high-level one ({@link ProgressSupport} and {@link CancelSupport}). + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class LifecycleBridge implements Lifecycle { + private final ProgressSupport progressHelper; + private final CancelSupport cancelSupport; + // may be null unless #start() is invoked + private Callback receiver; + private CancelledException cancellation; + + + public LifecycleBridge(ProgressSupport progress, CancelSupport cancel) { + progressHelper = progress; + cancelSupport = cancel; + } + + public void start(int count, Callback callback, Object token) { + receiver = callback; + if (progressHelper != null) { + progressHelper.start(count); + } + } + + public void finish(Object token) { + if (progressHelper != null) { + progressHelper.done(); + } + receiver = null; + } + + // XXX SHALL work without start/finish sequence because + // HgLogCommand invokes ChangesetTransformer#next directly (i.e. not from + // inside a library's #range() or similar) to process changesets in unnatural order. + public void nextStep() { + if (progressHelper != null) { + progressHelper.worked(1); + } + if (cancelSupport == null) { + return; + } + try { + cancelSupport.checkCancelled(); + } catch (CancelledException ex) { + if (receiver != null) { + receiver.stop(); + } + cancellation = ex; + } + } + + public void stop() { + if (receiver != null) { + receiver.stop(); + } + } + + /** + * @return true iff {@link CancelledException} was thrown and catched. Forced stop doesn't count + */ + public boolean isCancelled() { + return cancellation != null; + } + + public CancelledException getCancelOrigin() { + return cancellation; + } +} \ No newline at end of file diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/internal/LifecycleProxy.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/LifecycleProxy.java Fri Dec 21 21:20:26 2012 +0100 @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import org.tmatesoft.hg.util.Adaptable; + +/** + * Save callback and delegate to another lifecycle instance, if any + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class LifecycleProxy implements Lifecycle { + + private Callback lifecycleCallback; + private Lifecycle target; + + public LifecycleProxy() { + } + + public LifecycleProxy(Object delegate) { + init(delegate); + } + + + public void start(int count, Callback callback, Object token) { + lifecycleCallback = callback; + if (target != null) { + target.start(count, callback, token); + } + } + + public void finish(Object token) { + if (target != null) { + target.finish(token); + } + lifecycleCallback = null; + } + + public void init(Object delegate) { + target = Adaptable.Factory.getAdapter(delegate, Lifecycle.class, null); + } + + public void stop() { + assert lifecycleCallback != null; + if (lifecycleCallback != null) { + lifecycleCallback.stop(); + } + } +} \ No newline at end of file diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/internal/RevlogStream.java --- a/src/org/tmatesoft/hg/internal/RevlogStream.java Thu Dec 20 20:21:59 2012 +0100 +++ b/src/org/tmatesoft/hg/internal/RevlogStream.java Fri Dec 21 21:20:26 2012 +0100 @@ -29,6 +29,7 @@ import org.tmatesoft.hg.repo.HgInvalidRevisionException; import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Adaptable; /** @@ -382,6 +383,7 @@ private final boolean needData; private DataAccess daIndex = null, daData = null; private Lifecycle.BasicCallback cb = null; + private Lifecycle lifecycleListener = null; private int lastRevisionRead = BAD_REVISION; private DataAccess lastUserData; // next are to track two major bottlenecks - patch application and actual time spent in inspector @@ -399,28 +401,35 @@ if (needData && !inline) { daData = getDataStream(); } - if (inspector instanceof Lifecycle) { + lifecycleListener = Adaptable.Factory.getAdapter(inspector, Lifecycle.class, null); + if (lifecycleListener != null) { cb = new Lifecycle.BasicCallback(); - ((Lifecycle) inspector).start(totalWork, cb, cb); + lifecycleListener.start(totalWork, cb, cb); } // applyTime = inspectorTime = 0; // TIMING } + // invoked only once per instance public void finish() { if (lastUserData != null) { lastUserData.done(); lastUserData = null; } - if (inspector instanceof Lifecycle) { - ((Lifecycle) inspector).finish(cb); + if (lifecycleListener != null) { + lifecycleListener.finish(cb); + lifecycleListener = null; + cb = null; + } daIndex.done(); if (daData != null) { daData.done(); + daData = null; } // System.out.printf("applyTime:%d ms, inspectorTime: %d ms\n", applyTime, inspectorTime); // TIMING } + // may be invoked few times per instance life public boolean range(int start, int end) throws IOException { byte[] nodeidBuf = new byte[20]; int i; diff -r 934037edbea0 -r 1ee452f31187 src/org/tmatesoft/hg/repo/HgChangelog.java --- a/src/org/tmatesoft/hg/repo/HgChangelog.java Thu Dec 20 20:21:59 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/HgChangelog.java Fri Dec 21 21:20:26 2012 +0100 @@ -33,10 +33,11 @@ import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.Callback; import org.tmatesoft.hg.internal.DataAccess; -import org.tmatesoft.hg.internal.IterateControlMediator; import org.tmatesoft.hg.internal.Lifecycle; +import org.tmatesoft.hg.internal.LifecycleBridge; import org.tmatesoft.hg.internal.Pool; import org.tmatesoft.hg.internal.RevlogStream; +import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.ProgressSupport; @@ -357,19 +358,33 @@ } } - private static class RawCsetParser implements RevlogStream.Inspector, Lifecycle { + private static class RawCsetParser implements RevlogStream.Inspector, Adaptable { private final Inspector inspector; private final Pool usersPool; private final RawChangeset cset = new RawChangeset(); - private final ProgressSupport progressHelper; - private IterateControlMediator iterateControl; + // non-null when inspector uses high-level lifecycle entities (progress and/or cancel supports) + private final LifecycleBridge lifecycleStub; + // non-null when inspector relies on low-level lifecycle and is responsible + // to proceed any possible high-level entities himself. + private final Lifecycle inspectorLifecycle; public RawCsetParser(HgChangelog.Inspector delegate) { assert delegate != null; inspector = delegate; usersPool = new Pool(); - progressHelper = ProgressSupport.Factory.get(delegate); + inspectorLifecycle = Adaptable.Factory.getAdapter(delegate, Lifecycle.class, null); + if (inspectorLifecycle == null) { + ProgressSupport ph = Adaptable.Factory.getAdapter(delegate, ProgressSupport.class, null); + CancelSupport cs = Adaptable.Factory.getAdapter(delegate, CancelSupport.class, null); + if (cs != null || ph != null) { + lifecycleStub = new LifecycleBridge(ph, cs); + } else { + lifecycleStub = null; + } + } else { + lifecycleStub = null; + } } public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { @@ -378,26 +393,30 @@ cset.init(data, 0, data.length, usersPool); // XXX there's no guarantee for Changeset.Callback that distinct instance comes each time, consider instance reuse inspector.next(revisionNumber, Nodeid.fromBinary(nodeid, 0), cset); - progressHelper.worked(1); + if (lifecycleStub != null) { + lifecycleStub.nextStep(); + } } catch (HgInvalidDataFormatException ex) { throw ex.setRevisionIndex(revisionNumber); } catch (IOException ex) { // XXX need better exception, perhaps smth like HgChangelogException (extends HgInvalidControlFileException) throw new HgInvalidControlFileException("Failed reading changelog", ex, null).setRevisionIndex(revisionNumber); } - if (iterateControl != null) { - iterateControl.checkCancelled(); + } + + public T getAdapter(Class adapterClass) { + if (adapterClass == Lifecycle.class) { + if (inspectorLifecycle != null) { + return adapterClass.cast(inspectorLifecycle); + } + // reveal interest in lifecycle only when either progress or cancel support is there + // and inspector itself doesn't respond to lifecycle request + // lifecycleStub may still be null here (no progress and cancel), it's ok to cast(null) + return adapterClass.cast(lifecycleStub); + } + return Adaptable.Factory.getAdapter(inspector, adapterClass, null); } - public void start(int count, Callback callback, Object token) { - CancelSupport cs = CancelSupport.Factory.get(inspector, null); - iterateControl = cs == null ? null : new IterateControlMediator(cs, callback); - progressHelper.start(count); - } - - public void finish(Object token) { - progressHelper.done(); - } } } diff -r 934037edbea0 -r 1ee452f31187 test/org/tmatesoft/hg/test/ErrorCollectorExt.java --- a/test/org/tmatesoft/hg/test/ErrorCollectorExt.java Thu Dec 20 20:21:59 2012 +0100 +++ b/test/org/tmatesoft/hg/test/ErrorCollectorExt.java Fri Dec 21 21:20:26 2012 +0100 @@ -59,7 +59,7 @@ public void assertTrue(final boolean value) { assertTrue(null, value); } - + public void assertTrue(final String reason, final boolean value) { checkSucceeds(new Callable() { public Object call() throws Exception { @@ -72,4 +72,12 @@ public void assertEquals(T expected, T actual) { checkThat(null, actual, CoreMatchers.equalTo(expected)); } + + public void assertEquals(String reason, T expected, T actual) { + checkThat(reason, actual, CoreMatchers.equalTo(expected)); + } + + public void fail(String reason) { + addError(new AssertionError(reason)); + } } \ No newline at end of file diff -r 934037edbea0 -r 1ee452f31187 test/org/tmatesoft/hg/test/TestAuxUtilities.java --- a/test/org/tmatesoft/hg/test/TestAuxUtilities.java Thu Dec 20 20:21:59 2012 +0100 +++ b/test/org/tmatesoft/hg/test/TestAuxUtilities.java Fri Dec 21 21:20:26 2012 +0100 @@ -147,83 +147,81 @@ } } } + + static class CancelAtValue { + public int lastSeen; + public final int stopValue; + protected final CancelImpl cancelImpl = new CancelImpl(); + + protected CancelAtValue(int value) { + stopValue = value; + } + + protected void nextValue(int value) { + lastSeen = value; + if (value == stopValue) { + cancelImpl.stop(); + } + } + } @Test public void testChangelogCancelSupport() throws Exception { HgRepository repository = Configuration.get().find("branches-1"); // any repo with more revisions - class InspectorImplementsCancel implements HgChangelog.Inspector, CancelSupport { - public final int when2stop; - public int lastVisitet = 0; - private final CancelImpl cancelImpl = new CancelImpl(); + class InspectorImplementsCancel extends CancelAtValue implements HgChangelog.Inspector, CancelSupport { public InspectorImplementsCancel(int limit) { - when2stop = limit; + super(limit); } public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { - lastVisitet = revisionNumber; - if (revisionNumber == when2stop) { - cancelImpl.stop(); - } + nextValue(revisionNumber); } public void checkCancelled() throws CancelledException { cancelImpl.checkCancelled(); } }; - class InspectorImplementsAdaptable implements HgChangelog.Inspector, Adaptable { - public final int when2stop; - public int lastVisitet = 0; - private final CancelImpl cancelImpl = new CancelImpl(); - + class InspectorImplementsAdaptable extends CancelAtValue implements HgChangelog.Inspector, Adaptable { public InspectorImplementsAdaptable(int limit) { - when2stop = limit; + super(limit); } public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { - lastVisitet = revisionNumber; - if (revisionNumber == when2stop) { - cancelImpl.stop(); - } + nextValue(revisionNumber); } + public T getAdapter(Class adapterClass) { if (CancelSupport.class == adapterClass) { return adapterClass.cast(cancelImpl); } return null; } - } // InspectorImplementsCancel insp1; repository.getChangelog().all(insp1= new InspectorImplementsCancel(2)); - Assert.assertEquals(insp1.when2stop, insp1.lastVisitet); + Assert.assertEquals(insp1.stopValue, insp1.lastSeen); repository.getChangelog().all(insp1 = new InspectorImplementsCancel(12)); - Assert.assertEquals(insp1.when2stop, insp1.lastVisitet); + Assert.assertEquals(insp1.stopValue, insp1.lastSeen); // InspectorImplementsAdaptable insp2; repository.getChangelog().all(insp2= new InspectorImplementsAdaptable(3)); - Assert.assertEquals(insp2.when2stop, insp2.lastVisitet); + Assert.assertEquals(insp2.stopValue, insp2.lastSeen); repository.getChangelog().all(insp2 = new InspectorImplementsAdaptable(10)); - Assert.assertEquals(insp2.when2stop, insp2.lastVisitet); + Assert.assertEquals(insp2.stopValue, insp2.lastSeen); } @Test public void testManifestCancelSupport() throws Exception { HgRepository repository = Configuration.get().find("branches-1"); // any repo with as many revisions as possible - class InspectorImplementsAdaptable implements HgManifest.Inspector, Adaptable { - public final int when2stop; - public int lastVisitet = 0; - private final CancelImpl cancelImpl = new CancelImpl(); - + class InspectorImplementsAdaptable extends CancelAtValue implements HgManifest.Inspector, Adaptable { public InspectorImplementsAdaptable(int limit) { - when2stop = limit; + super(limit); } public boolean begin(int mainfestRevision, Nodeid nid, int changelogRevision) { - if (++lastVisitet == when2stop) { - cancelImpl.stop(); - } + nextValue(lastSeen+1); return true; } @@ -244,9 +242,9 @@ } InspectorImplementsAdaptable insp1; repository.getManifest().walk(0, TIP, insp1= new InspectorImplementsAdaptable(3)); - Assert.assertEquals(insp1.when2stop, insp1.lastVisitet); + Assert.assertEquals(insp1.stopValue, insp1.lastSeen); repository.getManifest().walk(0, TIP, insp1 = new InspectorImplementsAdaptable(10)); - Assert.assertEquals(insp1.when2stop, insp1.lastVisitet); + Assert.assertEquals(insp1.stopValue, insp1.lastSeen); } @Test diff -r 934037edbea0 -r 1ee452f31187 test/org/tmatesoft/hg/test/TestHistory.java --- a/test/org/tmatesoft/hg/test/TestHistory.java Thu Dec 20 20:21:59 2012 +0100 +++ b/test/org/tmatesoft/hg/test/TestHistory.java Fri Dec 21 21:20:26 2012 +0100 @@ -42,10 +42,13 @@ import org.tmatesoft.hg.core.HgLogCommand; import org.tmatesoft.hg.core.HgLogCommand.CollectHandler; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.AdapterPlug; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.test.LogOutputParser.Record; import org.tmatesoft.hg.util.Adaptable; +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.Path; @@ -93,7 +96,10 @@ changelogParser.reset(); eh.run("hg", "log", "--debug"); List r = new HgLogCommand(repo).execute(); - report("hg log - COMPLETE REPO HISTORY", r, true); + report("hg log - COMPLETE REPO HISTORY", r, true); + + r = new HgLogCommand(repo).debugSwitch1().execute(); + report("hg log - COMPLETE REPO HISTORY, FROM NEW TO OLD", r, false); } @Test @@ -193,8 +199,21 @@ String lastRevOfFname1 = "369c0882d477c11424a62eb4b791e86d1d4b6769"; errorCollector.assertEquals(lastRevOfFname1, h.lastChangesetReportedAtRename.get(0).getNodeid().toString()); report("HgChangesetHandler(renames: true, ancestry:false)", h.getChanges(), true); - - // TODO direction + // + // Direction + h = new CollectWithRenameHandler(); + new HgLogCommand(repo).file(fname2, true, false).debugSwitch1().execute(h); + // Identical rename shall be reported, at the same moment + errorCollector.assertEquals(1, h.renames.size()); + rename = h.renames.get(0); + errorCollector.assertEquals(fname1, rename.first().getPath().toString()); + errorCollector.assertEquals(fname2, rename.second().getPath().toString()); + errorCollector.assertEquals(1, h.lastChangesetReportedAtRename.size()); + // new to old, recently reported would be the very first revision fname2 pops up + String firstRevOfFname2 = "27e7a69373b74d42e75f3211e56510ff17d01370"; + errorCollector.assertEquals(firstRevOfFname2, h.lastChangesetReportedAtRename.get(0).getNodeid().toString()); + report("HgChangesetHandler(renames: true, ancestry:false)", h.getChanges(), false); + // // TODO TreeChangeHandler } @@ -228,8 +247,12 @@ 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 + // + // Direction + h = new CollectWithRenameHandler(); + new HgLogCommand(repo).file(fname2, false, true).debugSwitch1().execute(h); + report("HgChangesetHandler(renames: false, ancestry:true)", h.getChanges(), fname2Follow, false/*!!!*/, errorCollector); + // // TODO TreeChangeHandler } @@ -256,10 +279,80 @@ errorCollector.assertEquals(fname1BranchRevision, h.lastChangesetReportedAtRename.get(0).getNodeid().toString()); // finally, match output report("HgChangesetHandler(renames: true, ancestry:true)", h.getChanges(), true); - // TODO direction + // + // Switch direction and compare, order shall match that from console + h = new CollectWithRenameHandler(); + new HgLogCommand(repo).file(fname2, true, true).debugSwitch1().execute(h); + // Identical rename event shall be reported + errorCollector.assertEquals(1, h.renames.size()); + rename = h.renames.get(0); + errorCollector.assertEquals(fname1, rename.first().getPath().toString()); + errorCollector.assertEquals(fname2, rename.second().getPath().toString()); + // new to old, recently reported would be the very first revision fname2 pops up + String firstRevOfFname2 = "27e7a69373b74d42e75f3211e56510ff17d01370"; + errorCollector.assertEquals(firstRevOfFname2, h.lastChangesetReportedAtRename.get(0).getNodeid().toString()); + report("HgChangesetHandler(renames: true, ancestry:true)", h.getChanges(), false /*do not reorder console results !!!*/); + // // TreeChangeHandler in #testChangesetTreeFollowRenameAndAncestry } + /** + * @see TestAuxUtilities#testChangelogCancelSupport() + */ + @Test + public void testLogCommandCancelSupport() throws Exception { + repo = Configuration.get().find("branches-1"); // any repo with more revisions + class BaseCancel extends TestAuxUtilities.CancelAtValue implements HgChangesetHandler { + BaseCancel(int limit) { + super(limit); + } + public void cset(HgChangeset changeset) throws HgCallbackTargetException { + nextValue(changeset.getRevisionIndex()); + } + }; + class ImplementsCancel extends BaseCancel implements CancelSupport { + ImplementsCancel(int limit) { + super(limit); + } + public void checkCancelled() throws CancelledException { + cancelImpl.checkCancelled(); + } + }; + class AdaptsToCancel extends BaseCancel implements Adaptable { + AdaptsToCancel(int limit) { + super(limit); + } + public T getAdapter(Class adapterClass) { + if (adapterClass == CancelSupport.class) { + return adapterClass.cast(cancelImpl); + } + return null; + } + } + + BaseCancel insp = new ImplementsCancel(3); + try { + new HgLogCommand(repo).execute(insp); + errorCollector.fail("CancelSupport as implemented iface"); + } catch (CancelledException ex) { + errorCollector.assertEquals("CancelSupport as implemented iface", insp.stopValue, insp.lastSeen); + } + insp = new AdaptsToCancel(5); + try { + new HgLogCommand(repo).execute(insp); + errorCollector.fail("Adaptable to CancelSupport"); + } catch (CancelledException ex) { + errorCollector.assertEquals("Adaptable to CancelSupport", insp.stopValue, insp.lastSeen); + } + insp = new BaseCancel(9); + try { + new HgLogCommand(repo).set(insp.cancelImpl).execute(insp); + errorCollector.fail("cmd#set(CancelSupport)"); + } catch (CancelledException e) { + errorCollector.assertEquals("cmd#set(CancelSupport)", insp.stopValue, insp.lastSeen); + } + } + private void report(String what, List r, boolean reverseConsoleResult) { final List consoleResult = changelogParser.getResult(); report(what, r, consoleResult, reverseConsoleResult, errorCollector); @@ -278,6 +371,8 @@ break; } Record cr = consoleResultItr.next(); + // flags, not separate checkThat() because when lists are large, and do not match, + // number of failures may slow down test process significantly int x = cs.getRevisionIndex() == cr.changesetIndex ? 0x1 : 0; x |= cs.getDate().toString().equals(cr.date) ? 0x2 : 0; x |= cs.getNodeid().toString().equals(cr.changesetNodeid) ? 0x4 : 0; @@ -398,34 +493,6 @@ //// - private static class AdapterPlug implements Adaptable { - private final Map, Object> adapters = new HashMap, Object>(); - private final List> adapterUses = new ArrayList>(); - - public void attachAdapter(Class adapterClass, T instance) { - adapters.put(adapterClass, instance); - } - - public T getAdapter(Class adapterClass) { - Object instance = adapters.get(adapterClass); - if (instance != null) { - adapterUses.add(adapterClass); - return adapterClass.cast(instance); - } - return null; - } - - public int getAdapterUse(Class adapterClass) { - int uses = 0; - for (Class c : adapterUses) { - if (c == adapterClass) { - uses++; - } - } - return uses; - } - } - private final class TreeCollectHandler extends AdapterPlug implements HgChangesetTreeHandler { private final LinkedList cmdResult = new LinkedList(); private final boolean reverseResult;