# HG changeset patch # User Artem Tikhomirov # Date 1305586614 -7200 # Node ID 41a778e3fd31e37b514e0595442ca8f713e01c74 # Parent 4252faa556cd493583b1e52cb135f93030867320 Issue 5: Facilities for progress and cancellation. More specific exceptions. Exceptions from callbacks as RuntimeException diff -r 4252faa556cd -r 41a778e3fd31 cmdline/org/tmatesoft/hg/console/Incoming.java --- a/cmdline/org/tmatesoft/hg/console/Incoming.java Mon May 16 21:10:36 2011 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Incoming.java Tue May 17 00:56:54 2011 +0200 @@ -60,7 +60,7 @@ HgIncomingCommand cmd = hgRepo.createIncomingCommand(); cmd.against(hgRemote); // - List missing = cmd.executeLite(null); + List missing = cmd.executeLite(); Collections.reverse(missing); // useful to test output, from newer to older Outgoing.dump("Nodes to fetch:", missing); System.out.printf("Total: %d\n\n", missing.size()); diff -r 4252faa556cd -r 41a778e3fd31 cmdline/org/tmatesoft/hg/console/Log.java --- a/cmdline/org/tmatesoft/hg/console/Log.java Mon May 16 21:10:36 2011 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Log.java Tue May 17 00:56:54 2011 +0200 @@ -22,6 +22,8 @@ import org.tmatesoft.hg.core.HgLogCommand.FileRevision; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.ProgressSupport; /** @@ -39,6 +41,11 @@ System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); return; } + // + // in fact, neither cancel nor progress of any use, need them just to check comamnd API + final CancelSupport noCancel = CancelSupport.Factory.get(null); + final ProgressSupport noProgress = ProgressSupport.Factory.get(null); + // final Dump dump = new Dump(hgRepo); dump.complete(cmdLineOpts.getBoolean("--debug")); dump.verbose(cmdLineOpts.getBoolean("-v", "--verbose")); @@ -55,6 +62,7 @@ if (limit != -1) { cmd.limit(limit); } + cmd.set(noCancel).set(noProgress); List files = cmdLineOpts.getList(""); final long start = System.currentTimeMillis(); if (files.isEmpty()) { diff -r 4252faa556cd -r 41a778e3fd31 cmdline/org/tmatesoft/hg/console/Outgoing.java --- a/cmdline/org/tmatesoft/hg/console/Outgoing.java Mon May 16 21:10:36 2011 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Outgoing.java Tue May 17 00:56:54 2011 +0200 @@ -52,7 +52,7 @@ cmd.against(hgRemote); // find all local children of commonKnown - List result = cmd.executeLite(null); + List result = cmd.executeLite(); dump("Lite", result); // // diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/ChangesetTransformer.java --- a/src/org/tmatesoft/hg/core/ChangesetTransformer.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/ChangesetTransformer.java Tue May 17 00:56:54 2011 +0200 @@ -22,11 +22,15 @@ import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; +import org.tmatesoft.hg.util.ProgressSupport; /** * Bridges {@link HgChangelog.RawChangeset} with high-level {@link HgChangeset} API + * FIXME move to .internal * * @author Artem Tikhomirov * @author TMate Software Ltd. @@ -34,13 +38,21 @@ /*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector { private final HgChangesetHandler handler; private final HgChangeset changeset; + private final ProgressSupport progressHelper; + private final CancelSupport cancelHelper; private Set branches; + private HgCallbackTargetException failure; + private CancelledException cancellation; // repo and delegate can't be null, parent walker can - public ChangesetTransformer(HgRepository hgRepo, HgChangesetHandler delegate, HgChangelog.ParentWalker pw) { + // ps and cs can't be null + public ChangesetTransformer(HgRepository hgRepo, HgChangesetHandler delegate, HgChangelog.ParentWalker pw, ProgressSupport ps, CancelSupport cs) { if (hgRepo == null || delegate == null) { throw new IllegalArgumentException(); } + if (ps == null || cs == null) { + throw new IllegalArgumentException(); + } HgStatusCollector statusCollector = new HgStatusCollector(hgRepo); // files listed in a changeset don't need their names to be rewritten (they are normalized already) PathPool pp = new PathPool(new PathRewrite.Empty()); @@ -48,15 +60,41 @@ changeset = new HgChangeset(statusCollector, pp); changeset.setParentHelper(pw); handler = delegate; + cancelHelper = cs; + progressHelper = ps; } public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { + if (failure != null || cancellation != null) { + return; // FIXME need a better way to stop iterating revlog + } if (branches != null && !branches.contains(cset.branch())) { return; } changeset.init(revisionNumber, nodeid, cset); - handler.next(changeset); + try { + handler.next(changeset); + progressHelper.worked(1); + cancelHelper.checkCancelled(); + } catch (RuntimeException ex) { + failure = new HgCallbackTargetException(ex).setRevision(nodeid).setRevisionNumber(revisionNumber); + } catch (CancelledException ex) { + cancellation = ex; + } + } + + 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; + throw toThrow; + } } public void limitBranches(Set branches) { diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgAbstractCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgAbstractCommand.java Tue May 17 00:56:54 2011 +0200 @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2011 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.CancelSupport; +import org.tmatesoft.hg.util.ProgressSupport; + +/** + * intentionally package-local, might be removed or refactored in future + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +class HgAbstractCommand> implements ProgressSupport.Target, CancelSupport.Target { + private ProgressSupport progressHelper; + private CancelSupport cancelHelper; + + @SuppressWarnings("unchecked") + public T set(ProgressSupport ps) { + progressHelper = ps; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T set(CancelSupport cs) { + cancelHelper = cs; + return (T) this; + } + + // shall not return null + protected ProgressSupport getProgressSupport(Object context) { + if (progressHelper != null) { + return progressHelper; + } + return ProgressSupport.Factory.get(context); + } + + // shall not return null + protected CancelSupport getCancelSupport(Object context) { + if (cancelHelper != null) { + return cancelHelper; + } + return CancelSupport.Factory.get(context); + } + +} diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgCallbackTargetException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgCallbackTargetException.java Tue May 17 00:56:54 2011 +0200 @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2011 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 static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; + +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + +/** + * Checked exception that indicates errors in client code and tries to supply extra information about the context it occured in. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgCallbackTargetException extends HgException { + private int revNumber = BAD_REVISION; + private Nodeid revision; + private Path filename; + + /** + * @param cause can't be null + */ + public HgCallbackTargetException(Throwable cause) { + super((Throwable) null); + if (cause == null) { + throw new IllegalArgumentException(); + } + if (cause.getClass() == Wrap.class) { + // eliminate wrapper + initCause(cause.getCause()); + } else { + initCause(cause); + } + } + + /** + * @return not {@link HgRepository#BAD_REVISION} only when local revision number was supplied at the construction time + */ + public int getRevisionNumber() { + return revNumber; + } + + public HgCallbackTargetException setRevisionNumber(int rev) { + revNumber = rev; + return this; + } + + /** + * @return non-null only when revision was supplied at construction time + */ + public Nodeid getRevision() { + return revision; + } + + public HgCallbackTargetException setRevision(Nodeid r) { + revision = r; + return this; + } + + /** + * @return non-null only if file name was set at construction time + */ + public Path getFileName() { + return filename; + } + + public HgCallbackTargetException setFileName(Path name) { + filename = name; + return this; + } + + @SuppressWarnings("unchecked") + public T getTargetException() { + return (T) getCause(); + } + + /** + * Despite this exception is merely a way to give users access to their own exceptions, it may still supply + * valuable debugging information about what led to the error. + */ + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(); + if (filename != null) { + sb.append(filename); + sb.append(':'); + sb.append(' '); + } + if (revNumber != BAD_REVISION) { + sb.append(revNumber); + if (revision != null) { + sb.append(':'); + } + } + if (revision != null) { + sb.append(revision.shortNotation()); + } + return sb.toString(); + } + + /** + * Given the approach high-level handlers throw RuntimeExceptions to indicate errors, and + * a need to throw reasonable checked exception from client code, clients may utilize this class + * to get their checked exceptions unwrapped by {@link HgCallbackTargetException} and serve as that + * exception cause, eliminating {@link RuntimeException} mediator. + */ + public static final class Wrap extends RuntimeException { + + public Wrap(Throwable cause) { + super(cause); + if (cause == null) { + throw new IllegalArgumentException(); + } + } + } +} diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgCatCommand.java --- a/src/org/tmatesoft/hg/core/HgCatCommand.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgCatCommand.java Tue May 17 00:56:54 2011 +0200 @@ -35,7 +35,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgCatCommand { +public class HgCatCommand extends HgAbstractCommand { private final HgRepository repo; private Path file; @@ -110,7 +110,7 @@ } HgDataFile dataFile = repo.getFileNode(file); if (!dataFile.exists()) { - throw new HgDataStreamException(file.toString(), new FileNotFoundException(file.toString())); + throw new HgDataStreamException(file, new FileNotFoundException(file.toString())); } int revToExtract; if (revision != null) { diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgChangesetHandler.java --- a/src/org/tmatesoft/hg/core/HgChangesetHandler.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgChangesetHandler.java Tue May 17 00:56:54 2011 +0200 @@ -22,9 +22,11 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public interface HgChangesetHandler { +public interface HgChangesetHandler/*XXX perhaps, shall parameterize with exception clients can throw, like: */ { /** * @param changeset not necessarily a distinct instance each time, {@link HgChangeset#clone() clone()} if need a copy. + * @throws RuntimeException or any subclass thereof to indicate error. General contract is that RuntimeExceptions + * will be re-thrown wrapped into {@link HgCallbackTargetException}. */ - void next(HgChangeset changeset); + void next(HgChangeset changeset)/* throws E*/; } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgDataStreamException.java --- a/src/org/tmatesoft/hg/core/HgDataStreamException.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgDataStreamException.java Tue May 17 00:56:54 2011 +0200 @@ -17,6 +17,7 @@ package org.tmatesoft.hg.core; import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.util.Path; /** * Any erroneous state with @link {@link HgDataFile} input/output, read/write operations @@ -26,8 +27,19 @@ */ @SuppressWarnings("serial") public class HgDataStreamException extends HgException { + private final Path fname; - public HgDataStreamException(String message, Throwable cause) { + public HgDataStreamException(Path file, String message, Throwable cause) { super(message, cause); + fname = file; + } + + public HgDataStreamException(Path file, Throwable cause) { + super(cause); + fname = file; + } + + public Path getFileName() { + return fname; } } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgIncomingCommand.java --- a/src/org/tmatesoft/hg/core/HgIncomingCommand.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgIncomingCommand.java Tue May 17 00:56:54 2011 +0200 @@ -34,6 +34,7 @@ import org.tmatesoft.hg.repo.HgRemoteRepository; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.ProgressSupport; /** * Command to find out changes available in a remote repository, missing locally. @@ -41,7 +42,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgIncomingCommand { +public class HgIncomingCommand extends HgAbstractCommand { private final HgRepository localRepo; private HgRemoteRepository remoteRepo; @@ -98,15 +99,14 @@ * Lightweight check for incoming changes, gives only list of revisions to pull. * Reported changes are from any branch (limits set by {@link #branch(String)} are not taken into account. * - * @param context anything hg4j can use to get progress and/or cancel support * @return list of nodes present at remote and missing locally * @throws HgException * @throws CancelledException */ - public List executeLite(Object context) throws HgException, CancelledException { + public List executeLite() throws HgException, CancelledException { LinkedHashSet result = new LinkedHashSet(); - RepositoryComparator repoCompare = getComparator(context); - for (BranchChain bc : getMissingBranches(context)) { + RepositoryComparator repoCompare = getComparator(); + for (BranchChain bc : getMissingBranches()) { List missing = repoCompare.visitBranches(bc); HashSet common = new HashSet(); // ordering is irrelevant repoCompare.collectKnownRoots(bc, common); @@ -124,21 +124,21 @@ * @throws HgException * @throws CancelledException */ - public void executeFull(final HgChangesetHandler handler) throws HgException, CancelledException { + public void executeFull(final HgChangesetHandler handler) throws HgException/*FIXME specific type*/, HgCallbackTargetException, CancelledException { if (handler == null) { throw new IllegalArgumentException("Delegate can't be null"); } - final List common = getCommon(handler); + final List common = getCommon(); HgBundle changegroup = remoteRepo.getChanges(common); + final ProgressSupport ps = getProgressSupport(handler); try { + final ChangesetTransformer transformer = new ChangesetTransformer(localRepo, handler, getParentHelper(), ps, getCancelSupport(handler)); + transformer.limitBranches(branches); changegroup.changes(localRepo, new HgChangelog.Inspector() { private int localIndex; private final HgChangelog.ParentWalker parentHelper; - private final ChangesetTransformer transformer; { - transformer = new ChangesetTransformer(localRepo, handler, getParentHelper()); - transformer.limitBranches(branches); parentHelper = getParentHelper(); // new revisions, if any, would be added after all existing, and would get numbered started with last+1 localIndex = localRepo.getChangelog().getRevisionCount(); @@ -154,14 +154,17 @@ transformer.next(localIndex++, nodeid, cset); } }); + transformer.checkFailure(); } catch (IOException ex) { throw new HgException(ex); + } finally { + ps.done(); } } - private RepositoryComparator getComparator(Object context) throws HgException, CancelledException { + private RepositoryComparator getComparator() throws CancelledException { if (remoteRepo == null) { - throw new HgBadArgumentException("Shall specify remote repository to compare against", null); + throw new IllegalArgumentException("Shall specify remote repository to compare against", null); } if (comparator == null) { comparator = new RepositoryComparator(getParentHelper(), remoteRepo); @@ -178,20 +181,20 @@ return parentHelper; } - private List getMissingBranches(Object context) throws HgException, CancelledException { + private List getMissingBranches() throws HgRemoteConnectionException, CancelledException { if (missingBranches == null) { - missingBranches = getComparator(context).calculateMissingBranches(); + missingBranches = getComparator().calculateMissingBranches(); } return missingBranches; } - private List getCommon(Object context) throws HgException, CancelledException { + private List getCommon() throws HgRemoteConnectionException, CancelledException { // return getComparator(context).getCommon(); final LinkedHashSet common = new LinkedHashSet(); // XXX common can be obtained from repoCompare, but at the moment it would almost duplicate work of calculateMissingBranches // once I refactor latter, common shall be taken from repoCompare. - RepositoryComparator repoCompare = getComparator(context); - for (BranchChain bc : getMissingBranches(context)) { + RepositoryComparator repoCompare = getComparator(); + for (BranchChain bc : getMissingBranches()) { repoCompare.collectKnownRoots(bc, common); } return new LinkedList(common); diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgLogCommand.java --- a/src/org/tmatesoft/hg/core/HgLogCommand.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgLogCommand.java Tue May 17 00:56:54 2011 +0200 @@ -27,13 +27,14 @@ import java.util.Set; import java.util.TreeSet; +import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; -import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.ByteChannel; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.ProgressSupport; /** @@ -48,7 +49,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgLogCommand implements HgChangelog.Inspector { +public class HgLogCommand extends HgAbstractCommand implements HgChangelog.Inspector { private final HgRepository repo; private Set users; @@ -164,9 +165,17 @@ /** * Similar to {@link #execute(org.tmatesoft.hg.repo.RawChangeset.Inspector)}, collects and return result as a list. */ - public List execute() throws HgException { + public List execute() throws HgDataStreamException { CollectHandler collector = new CollectHandler(); - execute(collector); + try { + execute(collector); + } catch (HgCallbackTargetException ex) { + // can't happen as long as our CollectHandler doesn't throw any exception + throw new HgBadStateException(ex.getCause()); + } catch (CancelledException ex) { + // can't happen, see above + throw new HgBadStateException(ex); + } return collector.getChanges(); } @@ -176,13 +185,14 @@ * @throws IllegalArgumentException when inspector argument is null * @throws ConcurrentModificationException if this log command instance is already running */ - public void execute(HgChangesetHandler handler) throws HgException { + public void execute(HgChangesetHandler handler) throws HgDataStreamException, HgCallbackTargetException, CancelledException { if (handler == null) { throw new IllegalArgumentException(); } if (csetTransform != null) { throw new ConcurrentModificationException(); } + final ProgressSupport progressHelper = getProgressSupport(handler); try { count = 0; HgChangelog.ParentWalker pw = parentHelper; // leave it uninitialized unless we iterate whole repo @@ -191,19 +201,27 @@ } // 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); + csetTransform = new ChangesetTransformer(repo, handler, pw, progressHelper, getCancelSupport(handler)); if (file == null) { + progressHelper.start(endRev - startRev + 1); repo.getChangelog().range(startRev, endRev, this); + csetTransform.checkFailure(); } else { + progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/); HgDataFile fileNode = repo.getFileNode(file); fileNode.history(startRev, endRev, this); + csetTransform.checkFailure(); if (fileNode.isCopy()) { // even if we do not follow history, report file rename do { if (handler instanceof FileHistoryHandler) { FileRevision src = new FileRevision(repo, fileNode.getCopySourceRevision(), fileNode.getCopySourceName()); FileRevision dst = new FileRevision(repo, fileNode.getRevision(0), fileNode.getPath()); - ((FileHistoryHandler) handler).copy(src, dst); + try { + ((FileHistoryHandler) handler).copy(src, dst); + } catch (RuntimeException ex) { + throw new HgCallbackTargetException(ex).setRevision(fileNode.getCopySourceRevision()).setFileName(fileNode.getCopySourceName()); + } } if (limit > 0 && count >= limit) { // if limit reach, follow is useless. @@ -212,12 +230,14 @@ if (followHistory) { fileNode = repo.getFileNode(fileNode.getCopySourceName()); fileNode.history(this); + csetTransform.checkFailure(); } } while (followHistory && fileNode.isCopy()); } } } finally { csetTransform = null; + progressHelper.done(); } } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgManifestCommand.java --- a/src/org/tmatesoft/hg/core/HgManifestCommand.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgManifestCommand.java Tue May 17 00:56:54 2011 +0200 @@ -39,7 +39,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgManifestCommand { +public class HgManifestCommand extends HgAbstractCommand { private final HgRepository repo; private Path.Matcher matcher; diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgOutgoingCommand.java --- a/src/org/tmatesoft/hg/core/HgOutgoingCommand.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgOutgoingCommand.java Tue May 17 00:56:54 2011 +0200 @@ -24,7 +24,9 @@ import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgRemoteRepository; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.ProgressSupport; /** * Command to find out changes made in a local repository and missing at remote repository. @@ -32,7 +34,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgOutgoingCommand { +public class HgOutgoingCommand extends HgAbstractCommand { private final HgRepository localRepo; private HgRemoteRepository remoteRepo; @@ -90,11 +92,16 @@ * Lightweight check for outgoing changes. * Reported changes are from any branch (limits set by {@link #branch(String)} are not taken into account. * - * @param context * @return list on local nodes known to be missing at remote server */ - public List executeLite(Object context) throws HgException, CancelledException { - return getComparator(context).getLocalOnlyRevisions(); + public List executeLite() throws HgRemoteConnectionException, CancelledException { + final ProgressSupport ps = getProgressSupport(null); + try { + ps.start(10); + return getComparator(new ProgressSupport.Sub(ps, 5), getCancelSupport(null)).getLocalOnlyRevisions(); + } finally { + ps.done(); + } } /** @@ -102,22 +109,30 @@ * * @param handler delegate to process changes */ - public void executeFull(final HgChangesetHandler handler) throws HgException, CancelledException { + public void executeFull(final HgChangesetHandler handler) throws HgRemoteConnectionException, HgCallbackTargetException, CancelledException { if (handler == null) { throw new IllegalArgumentException("Delegate can't be null"); } - ChangesetTransformer inspector = new ChangesetTransformer(localRepo, handler, getParentHelper()); - inspector.limitBranches(branches); - getComparator(handler).visitLocalOnlyRevisions(inspector); + final ProgressSupport ps = getProgressSupport(handler); + final CancelSupport cs = getCancelSupport(handler); + try { + ps.start(-1); + ChangesetTransformer inspector = new ChangesetTransformer(localRepo, handler, getParentHelper(), ps, cs); + inspector.limitBranches(branches); + getComparator(new ProgressSupport.Sub(ps, 1), cs).visitLocalOnlyRevisions(inspector); + inspector.checkFailure(); + } finally { + ps.done(); + } } - private RepositoryComparator getComparator(Object context) throws HgException, CancelledException { + private RepositoryComparator getComparator(ProgressSupport ps, CancelSupport cs) throws HgRemoteConnectionException, CancelledException { if (remoteRepo == null) { throw new IllegalArgumentException("Shall specify remote repository to compare against"); } if (comparator == null) { comparator = new RepositoryComparator(getParentHelper(), remoteRepo); - comparator.compare(context); + comparator.compare(ps, cs); } return comparator; } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgRemoteConnectionException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgRemoteConnectionException.java Tue May 17 00:56:54 2011 +0200 @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2011 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; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgRemoteConnectionException extends HgException { + + private String serverInfo; + private String cmdName; + + public HgRemoteConnectionException(String reason) { + super(reason); + } + + public HgRemoteConnectionException(String reason, Throwable cause) { + super(reason, cause); + } + + public HgRemoteConnectionException setServerInfo(String si) { + serverInfo = si; + return this; + } + public String getServerInfo() { + return serverInfo; + } + + public HgRemoteConnectionException setRemoteCommand(String commandName) { + cmdName = commandName; + return this; + } + + public String getRemoteCommand() { + return cmdName; + } +} diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/core/HgStatusCommand.java --- a/src/org/tmatesoft/hg/core/HgStatusCommand.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgStatusCommand.java Tue May 17 00:56:54 2011 +0200 @@ -21,6 +21,7 @@ import static org.tmatesoft.hg.repo.HgRepository.*; import java.util.ConcurrentModificationException; +import java.util.concurrent.CancellationException; import org.tmatesoft.hg.internal.ChangelogHelper; import org.tmatesoft.hg.repo.HgRepository; @@ -36,7 +37,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgStatusCommand { +public class HgStatusCommand extends HgAbstractCommand { private final HgRepository repo; private int startRevision = TIP; @@ -161,7 +162,7 @@ * @throws IllegalArgumentException if handler is null * @throws ConcurrentModificationException if this command already runs (i.e. being used from another thread) */ - public void execute(Handler statusHandler) { + public void execute(Handler statusHandler) throws CancellationException, HgCallbackTargetException { if (statusHandler == null) { throw new IllegalArgumentException(); } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/internal/RepositoryComparator.java --- a/src/org/tmatesoft/hg/internal/RepositoryComparator.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/internal/RepositoryComparator.java Tue May 17 00:56:54 2011 +0200 @@ -31,6 +31,7 @@ import org.tmatesoft.hg.core.HgBadStateException; import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.HgRemoteConnectionException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgRemoteRepository; @@ -57,9 +58,7 @@ remoteRepo = hgRemote; } - public RepositoryComparator compare(Object context) throws HgException, CancelledException { - ProgressSupport progressSupport = ProgressSupport.Factory.get(context); - CancelSupport cancelSupport = CancelSupport.Factory.get(context); + public RepositoryComparator compare(ProgressSupport progressSupport, CancelSupport cancelSupport) throws HgRemoteConnectionException, CancelledException { cancelSupport.checkCancelled(); progressSupport.start(10); common = Collections.unmodifiableList(findCommonWithRemote()); @@ -126,7 +125,7 @@ changelog.range(earliestRevision+1, changelog.getLastRevision(), inspector); } - private List findCommonWithRemote() throws HgException { + private List findCommonWithRemote() throws HgRemoteConnectionException { List remoteHeads = remoteRepo.heads(); LinkedList resultCommon = new LinkedList(); // these remotes are known in local LinkedList toQuery = new LinkedList(); // these need further queries to find common @@ -208,7 +207,7 @@ } // somewhat similar to Outgoing.findCommonWithRemote() - public List calculateMissingBranches() throws HgException { + public List calculateMissingBranches() throws HgRemoteConnectionException { List remoteHeads = remoteRepo.heads(); LinkedList common = new LinkedList(); // these remotes are known in local LinkedList toQuery = new LinkedList(); // these need further queries to find common diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/repo/HgDataFile.java --- a/src/org/tmatesoft/hg/repo/HgDataFile.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgDataFile.java Tue May 17 00:56:54 2011 +0200 @@ -136,7 +136,7 @@ insp = new ContentPipe(sink, metadata.dataOffset(revision)); } else { // do not know if there's metadata - insp = new MetadataContentPipe(sink, metadata); + insp = new MetadataContentPipe(sink, metadata, getPath()); } insp.checkCancelled(); super.content.iterate(revision, revision, true, insp); @@ -146,7 +146,7 @@ throw ex; } catch (HgException ex) { // shall not happen, unless we changed ContentPipe or its subclass - throw new HgDataStreamException(ex.getClass().getName(), ex); + throw new HgDataStreamException(getPath(), ex.getClass().getName(), ex); } } @@ -210,7 +210,7 @@ } catch (CancelledException ex) { // it's ok, we did that } catch (Exception ex) { - throw new HgDataStreamException("Can't initialize metadata", ex); + throw new HgDataStreamException(getPath(), "Can't initialize metadata", ex); } } if (!metadata.known(0)) { @@ -319,10 +319,12 @@ private static class MetadataContentPipe extends ContentPipe { private final Metadata metadata; + private final Path fname; // need this only for error reporting - public MetadataContentPipe(ByteChannel sink, Metadata _metadata) { + public MetadataContentPipe(ByteChannel sink, Metadata _metadata, Path file) { super(sink, 0); metadata = _metadata; + fname = file; } @Override @@ -382,7 +384,7 @@ _metadata.trimToSize(); metadata.add(revisionNumber, lastEntryStart, _metadata); if (da.isEmpty() || !byteOne) { - throw new HgDataStreamException(String.format("Metadata for revision %d is not closed properly", revisionNumber), null); + throw new HgDataStreamException(fname, String.format("Metadata for revision %d is not closed properly", revisionNumber), null); } // da is in prepared state (i.e. we consumed all bytes up to metadata end). } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/repo/HgLookup.java --- a/src/org/tmatesoft/hg/repo/HgLookup.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgLookup.java Tue May 17 00:56:54 2011 +0200 @@ -70,7 +70,7 @@ } } - public HgBundle loadBundle(File location) throws HgException { + public HgBundle loadBundle(File location) /*XXX perhaps, HgDataStreamException or anything like HgMalformedDataException? Or RuntimeEx is better?*/{ if (location == null || !location.canRead()) { throw new IllegalArgumentException(); } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/repo/HgRemoteRepository.java --- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Tue May 17 00:56:54 2011 +0200 @@ -48,7 +48,7 @@ import org.tmatesoft.hg.core.HgBadArgumentException; import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.HgRemoteConnectionException; import org.tmatesoft.hg.core.Nodeid; /** @@ -115,7 +115,7 @@ } } - public boolean isInvalid() throws HgException { + public boolean isInvalid() throws HgRemoteConnectionException { // say hello to server, check response if (Boolean.FALSE.booleanValue()) { throw HgRepository.notImplemented(); @@ -137,7 +137,7 @@ } } - public List heads() throws HgException { + public List heads() throws HgRemoteConnectionException { try { URL u = new URL(url, url.getPath() + "?cmd=heads"); HttpURLConnection c = setupConnection(u.openConnection()); @@ -156,13 +156,13 @@ } return parseResult; } catch (MalformedURLException ex) { - throw new HgException(ex); + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("heads").setServerInfo(getLocation()); } catch (IOException ex) { - throw new HgException(ex); + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("heads").setServerInfo(getLocation()); } } - public List between(Nodeid tip, Nodeid base) throws HgException { + public List between(Nodeid tip, Nodeid base) throws HgRemoteConnectionException { Range r = new Range(base, tip); // XXX shall handle errors like no range key in the returned map, not sure how. return between(Collections.singletonList(r)).get(r); @@ -171,9 +171,9 @@ /** * @param ranges * @return map, where keys are input instances, values are corresponding server reply - * @throws HgException + * @throws HgRemoteConnectionException */ - public Map> between(Collection ranges) throws HgException { + public Map> between(Collection ranges) throws HgRemoteConnectionException { if (ranges.isEmpty()) { return Collections.emptyMap(); } @@ -257,13 +257,13 @@ is.close(); return rv; } catch (MalformedURLException ex) { - throw new HgException(ex); + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("between").setServerInfo(getLocation()); } catch (IOException ex) { - throw new HgException(ex); + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("between").setServerInfo(getLocation()); } } - public List branches(List nodes) throws HgException { + public List branches(List nodes) throws HgRemoteConnectionException { StringBuilder sb = new StringBuilder(20 + nodes.size() * 41); sb.append("nodes="); for (Nodeid n : nodes) { @@ -291,7 +291,7 @@ parseResult.add(Nodeid.fromAscii(st.sval)); } if (parseResult.size() != nodes.size() * 4) { - throw new HgException(String.format("Bad number of nodeids in result (shall be factor 4), expected %d, got %d", nodes.size()*4, parseResult.size())); + throw new HgRemoteConnectionException(String.format("Bad number of nodeids in result (shall be factor 4), expected %d, got %d", nodes.size()*4, parseResult.size())); } ArrayList rv = new ArrayList(nodes.size()); for (int i = 0; i < nodes.size(); i++) { @@ -300,9 +300,9 @@ } return rv; } catch (MalformedURLException ex) { - throw new HgException(ex); + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("branches").setServerInfo(getLocation()); } catch (IOException ex) { - throw new HgException(ex); + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("branches").setServerInfo(getLocation()); } } @@ -322,7 +322,7 @@ * (there's no header like HG10?? with the server output, though, * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat) */ - public HgBundle getChanges(List roots) throws HgException { + public HgBundle getChanges(List roots) throws HgRemoteConnectionException { List _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; StringBuilder sb = new StringBuilder(20 + _roots.size() * 41); sb.append("roots="); @@ -346,10 +346,11 @@ System.out.printf("Wrote bundle %s for roots %s\n", tf, sb); } return getLookupHelper().loadBundle(tf); - } catch (MalformedURLException ex) { - throw new HgException(ex); + } catch (MalformedURLException ex) { // XXX in fact, this exception might be better to be re-thrown as RuntimeEx, + // as there's little user can do about this issue (URLs are constructed by our code) + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); } catch (IOException ex) { - throw new HgException(ex); + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); } } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/util/CancelSupport.java --- a/src/org/tmatesoft/hg/util/CancelSupport.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/util/CancelSupport.java Tue May 17 00:56:54 2011 +0200 @@ -39,7 +39,7 @@ /** * Obtain non-null cancel support object. * - * @param target any object (or null) that might have cancel support + * @param target any object (or null) that might have cancel support. For null, returns an instance than never cancels. * @return target if it's capable checking cancellation status or no-op implementation that never cancels. */ public static CancelSupport get(Object target) { @@ -58,4 +58,8 @@ }; } } + + interface Target { + T set(CancelSupport cs); + } } diff -r 4252faa556cd -r 41a778e3fd31 src/org/tmatesoft/hg/util/ProgressSupport.java --- a/src/org/tmatesoft/hg/util/ProgressSupport.java Mon May 16 21:10:36 2011 +0200 +++ b/src/org/tmatesoft/hg/util/ProgressSupport.java Tue May 17 00:56:54 2011 +0200 @@ -24,8 +24,10 @@ */ public interface ProgressSupport { - public void start(long totalUnits); + // -1 for unspecified? + public void start(int totalUnits); public void worked(int units); + // XXX have to specify whether PS implementors may expect #done regardless of job completion (i.e. in case of cancellation) public void done(); static class Factory { @@ -45,7 +47,7 @@ } } return new ProgressSupport() { - public void start(long totalUnits) { + public void start(int totalUnits) { } public void worked(int units) { } @@ -54,4 +56,40 @@ }; } } + + class Sub implements ProgressSupport { + private final ProgressSupport ps; + private int total; + private int units; + private int psUnits; + + public Sub(ProgressSupport parent, int parentUnits) { + if (parent == null) { + throw new IllegalArgumentException(); + } + ps = parent; + psUnits = parentUnits; + } + + public void start(int totalUnits) { + total = totalUnits; + } + + public void worked(int worked) { + // FIXME fine-grained subprogress report. now only report at about 50% + if (psUnits > 1 && units < total/2 && units+worked > total/2) { + ps.worked(psUnits/2); + psUnits -= psUnits/2; + } + units += worked; + } + + public void done() { + ps.worked(psUnits); + } + } + + interface Target { + T set(ProgressSupport ps); + } } diff -r 4252faa556cd -r 41a778e3fd31 test/org/tmatesoft/hg/test/TestIncoming.java --- a/test/org/tmatesoft/hg/test/TestIncoming.java Mon May 16 21:10:36 2011 +0200 +++ b/test/org/tmatesoft/hg/test/TestIncoming.java Tue May 17 00:56:54 2011 +0200 @@ -92,7 +92,7 @@ ExecHelper eh = new ExecHelper(outParser, new File(localRepo.getLocation())); cmd.executeFull(collector); eh.run("hg", "incoming", "--debug", hgRemote.getLocation()); - List liteResult = cmd.executeLite(null); + List liteResult = cmd.executeLite(); report(collector, outParser, liteResult, errorCollector); return liteResult; } diff -r 4252faa556cd -r 41a778e3fd31 test/org/tmatesoft/hg/test/TestOutgoing.java --- a/test/org/tmatesoft/hg/test/TestOutgoing.java Mon May 16 21:10:36 2011 +0200 +++ b/test/org/tmatesoft/hg/test/TestOutgoing.java Tue May 17 00:56:54 2011 +0200 @@ -66,7 +66,7 @@ HgLogCommand.CollectHandler collector = new HgLogCommand.CollectHandler(); // cmd.executeFull(collector); - List liteResult = cmd.executeLite(null); + List liteResult = cmd.executeLite(); eh.run("hg", "outgoing", "--debug", hgRemote.getLocation()); TestIncoming.report(collector, outParser, liteResult, errorCollector); // @@ -79,7 +79,7 @@ // cmd = new HgOutgoingCommand(lookup.detect(dest)).against(hgRemote); cmd.executeFull(collector = new HgLogCommand.CollectHandler()); - liteResult = cmd.executeLite(null); + liteResult = cmd.executeLite(); outParser.reset(); eh.run("hg", "outgoing", "--debug", hgRemote.getLocation()); TestIncoming.report(collector, outParser, liteResult, errorCollector);