# HG changeset patch # User Artem Tikhomirov # Date 1355767567 -3600 # Node ID 5dcb4581c8ef0c01c388d7741fe17dc2477ee33e # Parent a41d955dc360b1c2b6af3e8dcb1f214ebc11bd6e Report renames when following file history tree with HgFileRenameHandlerMixin diff -r a41d955dc360 -r 5dcb4581c8ef cmdline/org/tmatesoft/hg/console/Log.java --- a/cmdline/org/tmatesoft/hg/console/Log.java Mon Dec 17 15:01:57 2012 +0100 +++ b/cmdline/org/tmatesoft/hg/console/Log.java Mon Dec 17 19:06:07 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2011 TMate Software Ltd + * Copyright (c) 2010-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 @@ -21,6 +21,7 @@ import java.util.List; import org.tmatesoft.hg.core.HgChangesetHandler; +import org.tmatesoft.hg.core.HgFileRenameHandlerMixin; import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.HgLogCommand; import org.tmatesoft.hg.repo.HgDataFile; @@ -121,12 +122,20 @@ } private static final class Dump extends ChangesetDumpHandler implements HgChangesetHandler.WithCopyHistory { + private final RenameDumpHandler renameHandlerDelegate; public Dump(HgRepository hgRepo) { super(hgRepo); + renameHandlerDelegate = new RenameDumpHandler(); } public void copy(HgFileRevision from, HgFileRevision to) { + renameHandlerDelegate.copy(from, to); + } + } + + static class RenameDumpHandler implements HgFileRenameHandlerMixin { + public void copy(HgFileRevision from, HgFileRevision to) { System.out.printf("Got notified that %s(%s) was originally known as %s(%s)\n", to.getPath(), to.getRevision(), from.getPath(), from.getRevision()); } } diff -r a41d955dc360 -r 5dcb4581c8ef cmdline/org/tmatesoft/hg/console/Main.java --- a/cmdline/org/tmatesoft/hg/console/Main.java Mon Dec 17 15:01:57 2012 +0100 +++ b/cmdline/org/tmatesoft/hg/console/Main.java Mon Dec 17 19:06:07 2012 +0100 @@ -30,6 +30,7 @@ import org.tmatesoft.hg.core.HgChangeset; import org.tmatesoft.hg.core.HgChangesetTreeHandler; import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.HgFileRenameHandlerMixin; import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.HgLogCommand; import org.tmatesoft.hg.core.HgManifestCommand; @@ -66,6 +67,7 @@ import org.tmatesoft.hg.repo.ext.HgExtensionsManager; import org.tmatesoft.hg.repo.ext.HgExtensionsManager.HgExt; import org.tmatesoft.hg.repo.ext.Rebase; +import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.FileWalker; import org.tmatesoft.hg.util.LogFacility; import org.tmatesoft.hg.util.Pair; @@ -174,7 +176,7 @@ HgLogCommand cmd = new HgLogCommand(hgRepo); cmd.file("file1b.txt", true); final int[] count = new int[] { 0 }; - cmd.execute(new HgChangesetTreeHandler() { + class MyHandler implements HgChangesetTreeHandler, Adaptable { public void treeElement(HgChangesetTreeHandler.TreeElement entry) { StringBuilder sb = new StringBuilder(); HashSet test = new HashSet(entry.childRevisions()); @@ -211,7 +213,20 @@ } count[0]++; } - }); + + public T getAdapter(Class adapterClass) { + if (adapterClass == HgFileRenameHandlerMixin.class) { + // in fact, new instance is not very nice, however + // getAdapter callers are supposed to understand the risk of new instance + // and cache returned value + // besides, stateless implementation of RenameDumpHandler + // doesn't really care about few instances + return adapterClass.cast(new Log.RenameDumpHandler()); + } + return null; + } + }; + cmd.execute(new MyHandler()); System.out.println(count[0]); final long end = System.nanoTime(); System.out.printf("buildFileLog: %,d ms\n", (end-start)/1000); diff -r a41d955dc360 -r 5dcb4581c8ef src/org/tmatesoft/hg/core/HgChangesetHandler.java --- a/src/org/tmatesoft/hg/core/HgChangesetHandler.java Mon Dec 17 15:01:57 2012 +0100 +++ b/src/org/tmatesoft/hg/core/HgChangesetHandler.java Mon Dec 17 19:06:07 2012 +0100 @@ -17,6 +17,7 @@ package org.tmatesoft.hg.core; import org.tmatesoft.hg.internal.Callback; +import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.Path; /** @@ -37,20 +38,17 @@ /** * When {@link HgLogCommand} is executed against file, handler passed to {@link HgLogCommand#execute(HgChangesetHandler)} may optionally - * implement this interface to get information about file renames. Method {@link #copy(HgFileRevision, HgFileRevision)} would - * get invoked prior any changeset of the original file (if file history being followed) is reported via {@link #cset(HgChangeset)}. + * implement this interface (or make it available through {@link Adaptable#getAdapter(Class)} to get information about file renames. + * Method {@link #copy(HgFileRevision, HgFileRevision)} would get invoked prior any changeset of the original file + * (if file history being followed) is reported via {@link #cset(HgChangeset)}. * * For {@link HgLogCommand#file(Path, boolean)} with renamed file path and follow argument set to false, * {@link #copy(HgFileRevision, HgFileRevision)} would be invoked for the first copy/rename in the history of the file, but not * followed by any changesets. + * + * @see HgFileRenameHandlerMixin */ @Callback - public interface WithCopyHistory extends HgChangesetHandler { - // XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them? - - /** - * @throws HgCallbackTargetException wrapper object for any exception user code may produce - */ - void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException; + public interface WithCopyHistory extends HgChangesetHandler, HgFileRenameHandlerMixin { } } diff -r a41d955dc360 -r 5dcb4581c8ef src/org/tmatesoft/hg/core/HgFileRenameHandlerMixin.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgFileRenameHandlerMixin.java Mon Dec 17 19:06:07 2012 +0100 @@ -0,0 +1,39 @@ +/* + * 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.core; + +import org.tmatesoft.hg.util.Adaptable; + +/** + * Addition to file history handlers (like {@link HgChangesetHandler} and {@link HgChangesetTreeHandler}) + * to receive notification about rename in the history of the file being walked. + * + * This mix-in shall be available from the host handler through the {@link Adaptable} mechanism, see + * {@link Adaptable.Factory#getAdapter(Object, Class, Object)}. Hence, implementing + * this interface in addition to host's would be the easiest way to achieve that. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface HgFileRenameHandlerMixin { + // XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them? + + /** + * @throws HgCallbackTargetException wrapper object for any exception user code may produce + */ + void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException; +} diff -r a41d955dc360 -r 5dcb4581c8ef src/org/tmatesoft/hg/core/HgFileRevision.java --- a/src/org/tmatesoft/hg/core/HgFileRevision.java Mon Dec 17 15:01:57 2012 +0100 +++ b/src/org/tmatesoft/hg/core/HgFileRevision.java Mon Dec 17 19:06:07 2012 +0100 @@ -69,10 +69,18 @@ origin = orig; } + public HgFileRevision(HgDataFile fileNode, Nodeid fileRevision, Path origin) { + this(fileNode.getRepo(), fileRevision, null, fileNode.getPath(), origin); + } + public Path getPath() { return path; } + /** + * Revision of the file + * @return never null + */ public Nodeid getRevision() { return revision; } diff -r a41d955dc360 -r 5dcb4581c8ef src/org/tmatesoft/hg/core/HgLogCommand.java --- a/src/org/tmatesoft/hg/core/HgLogCommand.java Mon Dec 17 15:01:57 2012 +0100 +++ b/src/org/tmatesoft/hg/core/HgLogCommand.java Mon Dec 17 19:06:07 2012 +0100 @@ -45,6 +45,7 @@ import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Pair; @@ -73,7 +74,17 @@ private int startRev = 0, endRev = TIP; private Calendar date; private Path file; - private boolean followHistory; // makes sense only when file != null + /* + * Whether to iterate file origins, if any. + * Makes sense only when file != null + */ + private boolean followRenames; + /* + * Whether to track history of the selected file version (based on file revision + * in working dir parent), follow ancestors only. + * Note, 'hg log --follow' combines both #followHistory and #followAncestry + */ + private boolean followAncestry; private ChangesetTransformer csetTransform; private HgParentChildMap parentHelper; @@ -184,7 +195,7 @@ public HgLogCommand file(Path file, boolean followCopyRename) { // multiple? Bad idea, would need to include extra method into Handler to tell start of next file this.file = file; - followHistory = followCopyRename; + followRenames = followAncestry = followCopyRename; return this; } @@ -256,24 +267,25 @@ // 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 (handler instanceof HgChangesetHandler.WithCopyHistory) { + 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()); - ((HgChangesetHandler.WithCopyHistory) handler).copy(src, dst); + withCopyHandler.copy(src, dst); } if (limit > 0 && count >= limit) { // if limit reach, follow is useless. break; } - if (followHistory) { + if (followRenames) { fileNode = repo.getFileNode(fileNode.getCopySourceName()); fileNode.history(this); csetTransform.checkFailure(); } - } while (followHistory && fileNode.isCopy()); + } while (followRenames && fileNode.isCopy()); } } } catch (HgRuntimeException ex) { @@ -306,44 +318,29 @@ } final ProgressSupport progressHelper = getProgressSupport(handler); final CancelSupport cancelHelper = getCancelSupport(handler, true); + final HgFileRenameHandlerMixin renameHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null); // builds tree of nodes according to parents in file's revlog - final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followHistory); + final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followRenames); // we iterate separate histories of each filename, need to connect // last node of historyA with first node of historyB (A renamed to B case) // to make overall history smooth. HistoryNode lastFromPrevIteration = null; + HgFileRevision copiedFrom = null, copiedTo = null; + boolean shallReportRenameAfter1Step = false; final int CACHE_CSET_IN_ADVANCE_THRESHOLD = 100; /* XXX is it really worth it? */ ElementImpl ei = null; // renamed files in the queue are placed with respect to #iterateDirection // i.e. if we iterate from new to old, recent filenames come first - LinkedList> fileRenamesQueue = buildFileRenamesQueue(); + List> fileRenamesQueue = buildFileRenamesQueue(); progressHelper.start(4 * fileRenamesQueue.size()); - do { + for (int namesIndex = 0, renamesQueueSize = fileRenamesQueue.size(); namesIndex < renamesQueueSize; namesIndex++) { - Pair renameInfo = fileRenamesQueue.removeFirst(); + final Pair renameInfo = fileRenamesQueue.get(namesIndex); cancelHelper.checkCancelled(); - HgDataFile fileNode = renameInfo.first(); - Nodeid fileLastRevToVisit = null; - if (followHistory) { - fileLastRevToVisit = renameInfo.second(); - if (fileLastRevToVisit == null) { - // it's either first or last item in the queue, depending on iteration order - assert fileRenamesQueue.isEmpty() || /*awful way to find out it's first iteration*/ lastFromPrevIteration == null; - // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex) - // or in the HgDataFile (getWorkingCopyOriginRevision) - Nodeid wdParentChangeset = repo.getWorkingCopyParents().first(); - if (!wdParentChangeset.isNull()) { - int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset); - fileLastRevToVisit = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath()); - } - // else fall-through, assume lastRevision() is ok here - } - } - int fileLastRevIndexToVisit = fileLastRevToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevToVisit); - final List changeHistory = treeBuildInspector.go(fileNode, fileLastRevIndexToVisit); + final List changeHistory = treeBuildInspector.go(renameInfo.first(), renameInfo.second()); assert changeHistory.size() > 0; progressHelper.worked(1); cancelHelper.checkCancelled(); @@ -370,25 +367,43 @@ // forward, from old to new: // A(0..n) -> B(0..m). First, report A(0)..A(n-1) // then A(n).bind(B(0)) - HistoryNode oldestOfTheNextChunk = changeHistory.get(0); - lastFromPrevIteration.bindChild(oldestOfTheNextChunk); + HistoryNode oldestOfTheNextChunk = changeHistory.get(0); // B(0) + lastFromPrevIteration.bindChild(oldestOfTheNextChunk); // lastFromPrevIteration is A(n) changeHistory.add(0, lastFromPrevIteration); + if (renameHandler != null) { // shall report renames + assert namesIndex > 0; + HgDataFile lastIterationFileNode = fileRenamesQueue.get(namesIndex-1).first(); // A + copiedFrom = new HgFileRevision(lastIterationFileNode, lastFromPrevIteration.fileRevision, null); + copiedTo = new HgFileRevision(renameInfo.first(), oldestOfTheNextChunk.fileRevision, copiedFrom.getPath()); + shallReportRenameAfter1Step = true; // report rename after A(n) + } } else { assert iterateDirection == IterateDirection.FromNewToOld; // A renamed to B. A(0..n) -> B(0..m). - // First, report B(m), B(m-1)...B(1), then A(n).bind(B(0)) + // First, report B(m), B(m-1)...B(1), then A(n).bind(B(0)), report B(0), A(n)... HistoryNode newestOfNextChunk = changeHistory.get(changeHistory.size() - 1); // A(n) newestOfNextChunk.bindChild(lastFromPrevIteration); changeHistory.add(lastFromPrevIteration); + if (renameHandler != null) { + assert namesIndex > 0; + // renameInfo points to chunk of name A now, and lastFromPrevIteration (from namesIndex-1) is B + copiedFrom = new HgFileRevision(renameInfo.first(), newestOfNextChunk.fileRevision, null); + HgDataFile lastIterationFileNode = fileRenamesQueue.get(namesIndex-1).first(); // B + copiedTo = new HgFileRevision(lastIterationFileNode, lastFromPrevIteration.fileRevision, copiedFrom.getPath()); + shallReportRenameAfter1Step = true; // report rename after B(0) + } } } - if (!fileRenamesQueue.isEmpty()) { + if (namesIndex + 1 < renamesQueueSize) { + // there's at least one more name we are going to look at, save + // one element for later binding + // if (iterateDirection == IterateDirection.FromOldToNew) { // save newest, and exclude it from this iteration (postpone for next) lastFromPrevIteration = changeHistory.remove(changeHistory.size()-1); } else { assert iterateDirection == IterateDirection.FromNewToOld; - // save oldest, and exclude it from thi iteration (postpone for next) + // save oldest, and exclude it from this iteration (postpone for next) lastFromPrevIteration = changeHistory.remove(0); } } else { @@ -407,8 +422,16 @@ handler.treeElement(ei.init(n)); ph2.worked(1); cancelHelper.checkCancelled(); + if (shallReportRenameAfter1Step) { + assert renameHandler != null; + assert copiedFrom != null; + assert copiedTo != null; + renameHandler.copy(copiedFrom, copiedTo); + shallReportRenameAfter1Step = false; + copiedFrom = copiedTo = null; + } } - } while (!fileRenamesQueue.isEmpty()); + } // for fileRenamesQueue; progressHelper.done(); } @@ -436,27 +459,39 @@ * Follows file renames and build a list of all corresponding file nodes and revisions they were * copied/renamed/branched at (IOW, their latest revision to look at). * - * If {@link #followHistory} is false, the list contains one element only, + * If {@link #followRenames} is false, the list contains one element only, * file node with the name of the file as it was specified by the user. * - * For the most recent file revision is null. + * For the most recent file revision depends on {@link #followAncestry}, and is file revision from working copy parent + * in it's true. null indicates file's TIP revision shall be used. * * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair * and possibly reuse this functionality * * @return list of file renames, ordered with respect to {@link #iterateDirection} */ - private LinkedList> buildFileRenamesQueue() { + private List> buildFileRenamesQueue() { LinkedList> rv = new LinkedList>(); - if (!followHistory) { - rv.add(new Pair(repo.getFileNode(file), null)); + Nodeid startRev = null; + HgDataFile fileNode = repo.getFileNode(file); + if (followAncestry) { + // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex) + // or in the HgDataFile (getWorkingCopyOriginRevision) + Nodeid wdParentChangeset = repo.getWorkingCopyParents().first(); + if (!wdParentChangeset.isNull()) { + int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset); + startRev = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath()); + } + // else fall-through, assume null (eventually, lastRevision()) is ok here + } + rv.add(new Pair(fileNode, startRev)); + if (!followRenames) { return rv; } - Path fp = file; - Nodeid copyRev = null; - boolean isCopy; - do { - HgDataFile fileNode = repo.getFileNode(fp); + while (fileNode.isCopy()) { + Path fp = fileNode.getCopySourceName(); + Nodeid copyRev = fileNode.getCopySourceRevision(); + fileNode = repo.getFileNode(fp); Pair p = new Pair(fileNode, copyRev); if (iterateDirection == IterateDirection.FromOldToNew) { rv.addFirst(p); @@ -464,11 +499,7 @@ assert iterateDirection == IterateDirection.FromNewToOld; rv.addLast(p); } - if (isCopy = fileNode.isCopy()) { - fp = fileNode.getCopySourceName(); - copyRev = fileNode.getCopySourceRevision(); - } - } while (isCopy); + }; return rv; } @@ -506,11 +537,12 @@ * @return list of history elements, from oldest to newest. In case {@link #followAncestry} is true, the list * is modifiable (to further augment with last/first elements of renamed file histories) */ - List go(HgDataFile fileNode, int lastRevisionIndex) throws HgInvalidControlFileException { + List go(HgDataFile fileNode, Nodeid fileLastRevisionToVisit) throws HgInvalidControlFileException { resultHistory = null; - completeHistory = new HistoryNode[lastRevisionIndex+1]; + int fileLastRevIndexToVisit = fileLastRevisionToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevisionToVisit); + completeHistory = new HistoryNode[fileLastRevIndexToVisit+1]; commitRevisions = new int[completeHistory.length]; - fileNode.indexWalk(0, lastRevisionIndex, this); + fileNode.indexWalk(0, fileLastRevIndexToVisit, this); if (!followAncestry) { // in case when ancestor not followed, it's safe to return unmodifiable list resultHistory = Arrays.asList(completeHistory); @@ -539,7 +571,7 @@ LinkedList strippedHistoryList = new LinkedList(); LinkedList queue = new LinkedList(); // look for ancestors of the selected history node - queue.add(completeHistory[lastRevisionIndex]); + queue.add(completeHistory[fileLastRevIndexToVisit]); do { HistoryNode withFileChange = queue.removeFirst(); if (strippedHistoryList.contains(withFileChange)) { diff -r a41d955dc360 -r 5dcb4581c8ef test/org/tmatesoft/hg/test/TestHistory.java --- a/test/org/tmatesoft/hg/test/TestHistory.java Mon Dec 17 15:01:57 2012 +0100 +++ b/test/org/tmatesoft/hg/test/TestHistory.java Mon Dec 17 19:06:07 2012 +0100 @@ -18,14 +18,17 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import org.junit.Rule; import org.junit.Test; @@ -33,6 +36,7 @@ import org.tmatesoft.hg.core.HgChangeset; import org.tmatesoft.hg.core.HgChangesetHandler; import org.tmatesoft.hg.core.HgChangesetTreeHandler; +import org.tmatesoft.hg.core.HgFileRenameHandlerMixin; import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.HgLogCommand; import org.tmatesoft.hg.core.HgLogCommand.CollectHandler; @@ -40,6 +44,7 @@ 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.Pair; import org.tmatesoft.hg.util.Path; @@ -142,12 +147,24 @@ final String fname = "file1_b"; assertTrue("[sanity]", repo.getFileNode(fname).exists()); eh.run("hg", "log", "--debug", "--follow", fname, "--cwd", repo.getLocation()); - + + final Map renames = new HashMap(); TreeCollectHandler h = new TreeCollectHandler(true); + h.attachAdapter(HgFileRenameHandlerMixin.class, new HgFileRenameHandlerMixin() { + + public void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException { + renames.put(from.getPath(), to.getPath()); + } + }); h.checkPrevInParents = true; new HgLogCommand(repo).file(fname, true).execute(h); + + assertEquals(1, h.getAdapterUse(HgFileRenameHandlerMixin.class)); report("execute with HgChangesetTreeHandler(follow == true)", h.getResult(), false); + + assertEquals(1, renames.size()); + assertEquals(Path.create(fname), renames.get(Path.create("file1_a"))); } private void report(String what, List r, boolean reverseConsoleResult) { @@ -288,7 +305,35 @@ //// - private final class TreeCollectHandler implements HgChangesetTreeHandler { + 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; boolean checkPrevInChildren = false;