changeset 698:822f3a83ff57

in, out and clone tests pass for ssh repositories. Infrastructure to decouple HgRemoteRepository from specific Connector implementation
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Tue, 06 Aug 2013 21:18:33 +0200
parents 24f4efedc9d5
children a483b2b68a2e
files src/org/tmatesoft/hg/core/HgIncomingCommand.java src/org/tmatesoft/hg/core/SessionContext.java src/org/tmatesoft/hg/internal/RepositoryComparator.java src/org/tmatesoft/hg/internal/remote/Connector.java src/org/tmatesoft/hg/internal/remote/HttpConnector.java src/org/tmatesoft/hg/internal/remote/RemoteConnectorDescriptor.java src/org/tmatesoft/hg/internal/remote/SshConnector.java src/org/tmatesoft/hg/repo/HgLookup.java src/org/tmatesoft/hg/repo/HgRemoteRepository.java test/org/tmatesoft/hg/test/TestIncoming.java
diffstat 10 files changed, 280 insertions(+), 87 deletions(-) [+]
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgIncomingCommand.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/src/org/tmatesoft/hg/core/HgIncomingCommand.java	Tue Aug 06 21:18:33 2013 +0200
@@ -155,9 +155,17 @@
 				
 				public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) throws HgRuntimeException {
 					if (parentHelper.knownNode(nodeid)) {
-						if (!common.contains(nodeid)) {
-							throw new HgInvalidStateException("Bundle shall not report known nodes other than roots we've supplied");
-						}
+						// FIXME getCommon() and remote.changegroup do not work together nicely.
+						// e.g. for hgtest-annotate-merge repository and TestIncoming, common reports r0 and r5 (ancestor of r5)
+						// because there's a distinct branch from r0 (in addition to those after r5). 
+						// remote.changegroup however answers with revisions that are children of either, 
+						/// so revisions 0..5 are reported as well and the next check fails. Instead, shall pass
+						// not common, but 'first to load' to remote.changegroup() or use another method (e.g. getbundle)
+						// Note, sending r5 only (i.e. checking for ancestors in common) won't help, changegroup sends children of
+						// requested roots only, and doesn't look for anything else
+//						if (!common.contains(nodeid)) {
+//							throw new HgInvalidStateException("Bundle shall not report known nodes other than roots we've supplied");
+//						}
 						return;
 					}
 					transformer.next(localIndex++, nodeid, cset);
--- a/src/org/tmatesoft/hg/core/SessionContext.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/src/org/tmatesoft/hg/core/SessionContext.java	Tue Aug 06 21:18:33 2013 +0200
@@ -16,6 +16,12 @@
  */
 package org.tmatesoft.hg.core;
 
+import java.net.URI;
+
+import org.tmatesoft.hg.internal.BasicSessionContext;
+import org.tmatesoft.hg.internal.Experimental;
+import org.tmatesoft.hg.internal.remote.RemoteConnectorDescriptor;
+import org.tmatesoft.hg.repo.HgLookup.RemoteDescriptor;
 import org.tmatesoft.hg.util.LogFacility;
 import org.tmatesoft.hg.util.Path;
 
@@ -67,6 +73,23 @@
 	}
 
 	/**
+	 * Work in progress, provisional API.
+	 * 
+	 * Provides descriptor that knows how to handle connections of specific kind
+	 * 
+	 * FIXME Perhaps, implementation here shall return null for any URI, while the one
+	 * in {@link BasicSessionContext} shall use our internal classes? However,
+	 * present implementation provides support for uris handled in the library itself, and likely
+	 * most clients need this, even if they supply own SessionContext
+	 *  
+	 * @return <code>null</code> if supplied URI doesn't point to a remote repository or repositories of that kind are not supported
+	 */
+	@Experimental(reason="Work in progress, provisional API")
+	public RemoteDescriptor getRemoteDescriptor(URI uri) {
+		return new RemoteConnectorDescriptor.Provider().get(this, uri);
+	}
+
+	/**
 	 * Providers of the context may implement
 	 */
 	public interface Source {
--- a/src/org/tmatesoft/hg/internal/RepositoryComparator.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/RepositoryComparator.java	Tue Aug 06 21:18:33 2013 +0200
@@ -23,6 +23,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
@@ -201,7 +202,7 @@
 	public List<BranchChain> calculateMissingBranches() throws HgRemoteConnectionException {
 		List<Nodeid> remoteHeads = remoteRepo.heads();
 		LinkedList<Nodeid> common = new LinkedList<Nodeid>(); // these remotes are known in local
-		LinkedList<Nodeid> toQuery = new LinkedList<Nodeid>(); // these need further queries to find common
+		LinkedHashSet<Nodeid> toQuery = new LinkedHashSet<Nodeid>(); // these need further queries to find common
 		for (Nodeid rh : remoteHeads) {
 			if (localRepo.knownNode(rh)) {
 				common.add(rh);
@@ -218,7 +219,7 @@
 		// records relation between branch head and its parent branch, if any
 		HashMap<Nodeid, BranchChain> head2chain = new HashMap<Nodeid, BranchChain>();
 		while (!toQuery.isEmpty()) {
-			List<RemoteBranch> remoteBranches = remoteRepo.branches(toQuery);	//head, root, first parent, second parent
+			List<RemoteBranch> remoteBranches = remoteRepo.branches(new ArrayList<Nodeid>(toQuery));	//head, root, first parent, second parent
 			toQuery.clear();
 			while(!remoteBranches.isEmpty()) {
 				RemoteBranch rb = remoteBranches.remove(0);
@@ -240,10 +241,10 @@
 					if (hasP1 && !localRepo.knownNode(rb.p1)) {
 						toQuery.add(rb.p1);
 						// we might have seen parent node already, and recorded it as a branch chain
-						// we shall reuse existing BC to get it completely initializer (head2chain map
+						// we shall reuse existing BC to get it completely initialized (head2chain map
 						// on second put with the same key would leave first BC uninitialized.
 						
-						// It seems there's no reason to be affraid (XXX although shall double-check)
+						// It seems there's no reason to be afraid (XXX although shall double-check)
 						// that BC's chain would get corrupt (its p1 and p2 fields assigned twice with different values)
 						// as parents are always the same (and likely, BC that is common would be the last unknown)
 						BranchChain bc = head2chain.get(rb.p1);
@@ -352,7 +353,7 @@
 
 		@Override
 		public String toString() {
-			return String.format("BranchChain [%s, %s]", branchRoot, branchHead);
+			return String.format("BranchChain [root:%s, head:%s]", branchRoot, branchHead);
 		}
 
 		void dump() {
--- a/src/org/tmatesoft/hg/internal/remote/Connector.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/remote/Connector.java	Tue Aug 06 21:18:33 2013 +0200
@@ -18,15 +18,15 @@
 
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.URL;
+import java.net.URI;
 import java.util.Collection;
 import java.util.List;
 
 import org.tmatesoft.hg.core.HgRemoteConnectionException;
 import org.tmatesoft.hg.core.Nodeid;
 import org.tmatesoft.hg.core.SessionContext;
+import org.tmatesoft.hg.repo.HgRemoteRepository.Range;
 import org.tmatesoft.hg.repo.HgRuntimeException;
-import org.tmatesoft.hg.repo.HgRemoteRepository.Range;
 
 /**
  * 
@@ -46,7 +46,7 @@
 	static final String NS_BOOKMARKS = "bookmarks";
 	static final String NS_PHASES = "phases";
 	
-	void init(URL url, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException;
+	void init(URI uri, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException;
 	String getServerLocation();
 	//
 	void connect() throws HgRemoteConnectionException, HgRuntimeException;
--- a/src/org/tmatesoft/hg/internal/remote/HttpConnector.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/remote/HttpConnector.java	Tue Aug 06 21:18:33 2013 +0200
@@ -28,6 +28,7 @@
 import java.io.SequenceInputStream;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import java.net.URLConnection;
 import java.security.cert.CertificateException;
@@ -56,6 +57,7 @@
  * @author TMate Software Ltd.
  */
 public class HttpConnector implements Connector {
+	private URI uri;
 	private URL url;
 	private SSLContext sslContext;
 	private String authInfo;
@@ -64,16 +66,16 @@
 	//
 	private HttpURLConnection conn;
 
-	public void init(URL url, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException {
-		this.url = url;
+	public void init(URI uri, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException {
+		this.uri = uri;
 		sessionCtx = sessionContext;
 		debug = new PropertyMarshal(sessionCtx).getBoolean("hg4j.remote.debug", false);
-		if (url.getUserInfo() != null) {
+		if (uri.getUserInfo() != null) {
 			String ai = null;
 			try {
 				// Hack to get Base64-encoded credentials
 				Preferences tempNode = Preferences.userRoot().node("xxx");
-				tempNode.putByteArray("xxx", url.getUserInfo().getBytes());
+				tempNode.putByteArray("xxx", uri.getUserInfo().getBytes());
 				ai = tempNode.get("xxx", null);
 				tempNode.removeNode();
 			} catch (BackingStoreException ex) {
@@ -87,6 +89,11 @@
 	}
 	
 	public void connect() throws HgRemoteConnectionException, HgRuntimeException {
+		try {
+			url = uri.toURL();
+		} catch (MalformedURLException ex) {
+			throw new HgRemoteConnectionException("Bad URL", ex);
+		}
 		if ("https".equals(url.getProtocol())) {
 			try {
 				sslContext = SSLContext.getInstance("SSL");
@@ -132,13 +139,13 @@
 	}
 	
 	public String getServerLocation() {
-		if (url.getUserInfo() == null) {
-			return url.toExternalForm();
+		if (uri.getUserInfo() == null) {
+			return uri.toString();
 		}
-		if (url.getPort() != -1) {
-			return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath());
+		if (uri.getPort() != -1) {
+			return String.format("%s://%s:%d%s", uri.getScheme(), uri.getHost(), uri.getPort(), uri.getPath());
 		} else {
-			return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath());
+			return String.format("%s://%s%s", uri.getScheme(), uri.getHost(), uri.getPath());
 		}
 	}
 
@@ -294,7 +301,6 @@
 					super.close();
 					if (debug) {
 						dumpResponseHeader(u);
-						dumpResponse();
 					}
 					try {
 						checkResponseOk("Push", CMD_UNBUNDLE);
@@ -395,11 +401,4 @@
 			System.out.printf("%s: %s\n", s, conn.getHeaderField(s));
 		}
 	}
-	
-	private void dumpResponse() throws IOException {
-		if (conn.getContentLength() > 0) {
-			final Object content = conn.getContent();
-			System.out.println(content);
-		}
-	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/remote/RemoteConnectorDescriptor.java	Tue Aug 06 21:18:33 2013 +0200
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2013 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.remote;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.tmatesoft.hg.core.HgBadArgumentException;
+import org.tmatesoft.hg.core.SessionContext;
+import org.tmatesoft.hg.repo.HgLookup.Authenticator;
+import org.tmatesoft.hg.repo.HgLookup.RemoteDescriptor;
+import org.tmatesoft.hg.repo.HgRemoteRepository;
+import org.tmatesoft.hg.util.Pair;
+
+/**
+ * Connector-aware descriptor of remote repository, i.e. descriptor that uses 
+ * {@link Connector Connectors} to connect to a remote repository.
+ * 
+ * <p>Candidate to become public API, with createConnector() method, so that {@link HgRemoteRepository} 
+ * may accept instances of that interfact directly
+ * 
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+public class RemoteConnectorDescriptor implements RemoteDescriptor {
+	
+	private Map<String, Pair<ClassLoader, String>> connFactory;
+	private final URI uri;
+
+	public RemoteConnectorDescriptor(Map<String,Pair<ClassLoader, String>> scheme2connectorMap, URI uriRemote) {
+		this(uriRemote);
+		connFactory = scheme2connectorMap;
+	}
+	
+	protected RemoteConnectorDescriptor(URI uriRemote) {
+		uri = uriRemote;
+	}
+
+	public URI getURI() {
+		return uri;
+	}
+
+	public Authenticator getAuth() {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	public Connector createConnector() throws HgBadArgumentException {
+		Pair<ClassLoader, String> connectorToBe = connFactory.get(uri.getScheme());
+		if (connectorToBe == null || connectorToBe.second() == null) {
+			throw new HgBadArgumentException(String.format("Can't instantiate connector for scheme '%s'", uri.getScheme()), null);
+		}
+		try {
+			Class<?> connClass = connectorToBe.first().loadClass(connectorToBe.second());
+			if (!Connector.class.isAssignableFrom(connClass)) {
+				throw new HgBadArgumentException(String.format("Connector %s for scheme '%s' is not a subclass of %s", connectorToBe.second(), uri.getScheme(), Connector.class.getName()), null);
+			}
+			final Object connector = connClass.newInstance();
+			return Connector.class.cast(connector);
+		} catch (ClassNotFoundException ex) {
+			throw new HgBadArgumentException(String.format("Can't instantiate connector %s for scheme '%s'", connectorToBe.second(), uri.getScheme()), ex);
+		} catch (InstantiationException ex) {
+			throw new HgBadArgumentException(String.format("Can't instantiate connector %s for scheme '%s'", connectorToBe.second(), uri.getScheme()), ex);
+		} catch (IllegalAccessException ex) {
+			throw new HgBadArgumentException(String.format("Can't instantiate connector %s for scheme '%s'", connectorToBe.second(), uri.getScheme()), ex);
+		}
+	}
+
+	// I don't see a reason to expose provider of RemoteDescriptors yet
+	// although it might not be the best idea for session context to serve as provider intermediate
+	public static class Provider {
+		private final Map<String, Pair<ClassLoader, String>> knownConnectors = new HashMap<String, Pair<ClassLoader, String>>(5);
+		
+		{
+			final ClassLoader cl = Provider.class.getClassLoader();
+			knownConnectors.put("http", new Pair<ClassLoader, String>(cl, HttpConnector.class.getName()));
+			knownConnectors.put("https", new Pair<ClassLoader, String>(cl, HttpConnector.class.getName()));
+			 // FIXME replace SshConnector.class with fqn string to avoid dependency from the trilead library in runtime
+			knownConnectors.put("ssh", new Pair<ClassLoader, String>(cl, SshConnector.class.getName()));
+		}
+
+		public RemoteDescriptor get(SessionContext ctx, URI uri) {
+			if (knownConnectors.containsKey(uri.getScheme())) {
+				return new RemoteConnectorDescriptor(knownConnectors, uri);
+			}
+			return null;
+		}
+	}
+}
--- a/src/org/tmatesoft/hg/internal/remote/SshConnector.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/src/org/tmatesoft/hg/internal/remote/SshConnector.java	Tue Aug 06 21:18:33 2013 +0200
@@ -28,7 +28,7 @@
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.SequenceInputStream;
-import java.net.URL;
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -54,7 +54,7 @@
  */
 public class SshConnector implements Connector {
 	private SessionContext sessionCtx;
-	private URL url;
+	private URI uri;
 	private Connection conn;
 	private Session session;
 	private int sessionUse;
@@ -62,14 +62,14 @@
 	private StreamGobbler remoteErr, remoteOut;
 	private OutputStream remoteIn;
 	
-	public void init(URL url, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException {
+	public void init(URI uri, SessionContext sessionContext, Object globalConfig) throws HgRuntimeException {
 		sessionCtx = sessionContext;
-		this.url = url;
+		this.uri = uri;
 	}
 	
 	public void connect() throws HgRemoteConnectionException, HgRuntimeException {
 		try {
-			conn = new Connection(url.getHost(), url.getPort() == -1 ? 22 : url.getPort());
+			conn = new Connection(uri.getHost(), uri.getPort() == -1 ? 22 : uri.getPort());
 			conn.connect();
 		} catch (IOException ex) {
 			throw new HgRemoteConnectionException("Failed to establish connection");
@@ -101,7 +101,7 @@
 		}
 		try {
 			session = conn.openSession();
-			final String path = url.getPath();
+			final String path = uri.getPath();
 			session.execCommand(String.format("hg -R %s serve --stdio", path.charAt(0) == '/' ? path.substring(1) : path));
 			remoteErr = new StreamGobbler(session.getStderr());
 			remoteOut = new StreamGobbler(session.getStdout());
@@ -123,7 +123,7 @@
 	}
 
 	public String getServerLocation() {
-		return url.toString(); // FIXME
+		return uri.toString(); // FIXME
 	}
 	
 	public String getCapabilities() throws HgRemoteConnectionException {
--- a/src/org/tmatesoft/hg/repo/HgLookup.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgLookup.java	Tue Aug 06 21:18:33 2013 +0200
@@ -19,8 +19,12 @@
 import static org.tmatesoft.hg.util.LogFacility.Severity.Warn;
 
 import java.io.File;
-import java.net.MalformedURLException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
 
 import org.tmatesoft.hg.core.HgBadArgumentException;
 import org.tmatesoft.hg.core.HgIOException;
@@ -29,7 +33,7 @@
 import org.tmatesoft.hg.internal.BasicSessionContext;
 import org.tmatesoft.hg.internal.ConfigFile;
 import org.tmatesoft.hg.internal.DataAccessProvider;
-import org.tmatesoft.hg.internal.Internals;
+import org.tmatesoft.hg.internal.Experimental;
 import org.tmatesoft.hg.internal.RequiresFile;
 import org.tmatesoft.hg.repo.HgRepoConfig.PathsSection;
 
@@ -112,16 +116,16 @@
 	 * @throws HgBadArgumentException if anything is wrong with the remote server's URL
 	 */
 	public HgRemoteRepository detectRemote(String key, HgRepository hgRepo) throws HgBadArgumentException {
-		URL url;
+		URI uri;
 		Exception toReport;
 		try {
-			url = new URL(key);
+			uri = new URI(key);
 			toReport = null;
-		} catch (MalformedURLException ex) {
-			url = null;
+		} catch (URISyntaxException ex) {
+			uri = null;
 			toReport = ex;
 		}
-		if (url == null) {
+		if (uri == null) {
 			String server = null;
 			if (hgRepo != null && !hgRepo.isInvalid()) {
 				PathsSection ps = hgRepo.getConfiguration().getPaths();
@@ -136,22 +140,55 @@
 				throw new HgBadArgumentException(String.format("Can't find server %s specification in the config", key), toReport);
 			}
 			try {
-				url = new URL(server);
-			} catch (MalformedURLException ex) {
+				uri = new URI(server);
+			} catch (URISyntaxException ex) {
 				throw new HgBadArgumentException(String.format("Found %s server spec in the config, but failed to initialize with it", key), ex);
 			}
 		}
-		return new HgRemoteRepository(getSessionContext(), url);
+		return detectRemote(uri);
 	}
 	
+	/**
+	 * Detect remote repository
+	 * <p>Use of this method is discouraged, please use {@link #detectRemote(URI)} instead}
+	 * 
+	 * @param url location of remote repository
+	 * @return instance to interact with remote repository
+	 * @throws HgBadArgumentException if location format is not a valid {@link URI}
+	 * @throws IllegalArgumentException if url is <code>null</code>
+	 */
 	public HgRemoteRepository detect(URL url) throws HgBadArgumentException {
 		if (url == null) {
 			throw new IllegalArgumentException();
 		}
-		if (Boolean.FALSE.booleanValue()) {
-			throw Internals.notImplemented();
+		try {
+			return detectRemote(url.toURI());
+		} catch (URISyntaxException ex) {
+			throw new HgBadArgumentException(String.format("Bad remote repository location: %s", url), ex);
 		}
-		return new HgRemoteRepository(getSessionContext(), url);
+	}
+	
+	/**
+	 * Resolves location of remote repository.
+	 * 
+	 * <p>Choice between {@link URI URIs} and {@link URL URLs} done in favor of former because they are purely syntactical,
+	 * while latter have semantics of {@link URL#openConnection() connection} establishing, which is not always possible.
+	 * E.g. one can't instantiate <code>new URL("ssh://localhost/")</code> as long as there's no local {@link URLStreamHandler} 
+	 * for the protocol, which is the case for certain JREs out there. The way {@link URLStreamHandlerFactory URLStreamHandlerFactories} 
+	 * are installed (static field/method) is quite fragile, and definitely not the one to rely on from a library's code (apps can carefully
+	 * do this, but library can't install own nor expect anyone outside there would do). Use of fake {@link URLStreamHandler} (fails to open
+	 * every connection) is possible, of course, although it seems to be less appropriate when there is alternative with {@link URI URIs}.
+	 * 
+	 * @param uriRemote remote repository location
+	 * @return instance to interact with remote repository
+	 * @throws HgBadArgumentException
+	 */
+	public HgRemoteRepository detectRemote(URI uriRemote) throws HgBadArgumentException {
+		RemoteDescriptor rd = getSessionContext().getRemoteDescriptor(uriRemote);
+		if (rd == null) {
+			throw new HgBadArgumentException(String.format("Unsupported remote repository location:%s", uriRemote), null);
+		}
+		return new HgRemoteRepository(getSessionContext(), rd);
 	}
 
 	private ConfigFile getGlobalConfig() {
@@ -173,4 +210,41 @@
 		}
 		return sessionContext;
 	}
+
+	
+	/**
+	 * Session context  ({@link SessionContext#getRemoteDescriptor(URI)} gives descriptor of remote when asked.
+	 */
+	@Experimental(reason="Work in progress")
+	public interface RemoteDescriptor {
+		URI getURI();
+		Authenticator getAuth();
+	}
+	
+	@Experimental(reason="Work in progress")
+	public interface Authenticator {
+		public void authenticate(RemoteDescriptor remote, AuthMethod authMethod);
+	}
+
+	/**
+	 * Clients do not implement this interface, instead, they invoke appropriate authentication method
+	 * once they got user input
+	 */
+	@Experimental(reason="Work in progress")
+	public interface AuthMethod {
+		public void noCredentials() throws AuthFailedException;
+		public boolean supportsPassword();
+		public void withPassword(String username, byte[] password) throws AuthFailedException;
+		public boolean supportsPublicKey();
+		public void withPublicKey(String username, InputStream publicKey, String passphrase) throws AuthFailedException;
+		public boolean supportsCertificate();
+		public void withCertificate() throws AuthFailedException;
+	}
+	
+	@SuppressWarnings("serial")
+	public class AuthFailedException extends Exception /*XXX HgRemoteException?*/ {
+		public AuthFailedException(String message, Throwable cause) {
+			super(message, cause);
+		}
+	}
 }
--- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java	Tue Aug 06 21:18:33 2013 +0200
@@ -21,17 +21,12 @@
 import static org.tmatesoft.hg.util.Outcome.Kind.Success;
 
 import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.StreamTokenizer;
-import java.net.ContentHandler;
-import java.net.ContentHandlerFactory;
-import java.net.URL;
-import java.net.URLConnection;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -58,8 +53,8 @@
 import org.tmatesoft.hg.internal.Internals;
 import org.tmatesoft.hg.internal.PropertyMarshal;
 import org.tmatesoft.hg.internal.remote.Connector;
-import org.tmatesoft.hg.internal.remote.HttpConnector;
-import org.tmatesoft.hg.internal.remote.SshConnector;
+import org.tmatesoft.hg.internal.remote.RemoteConnectorDescriptor;
+import org.tmatesoft.hg.repo.HgLookup.RemoteDescriptor;
 import org.tmatesoft.hg.util.LogFacility.Severity;
 import org.tmatesoft.hg.util.Outcome;
 import org.tmatesoft.hg.util.Pair;
@@ -81,41 +76,14 @@
 	private Set<String> remoteCapabilities;
 	private Connector remote;
 	
-	static {
-		URLConnection.setContentHandlerFactory(new ContentHandlerFactory() {
-			
-			public ContentHandler createContentHandler(String mimetype) {
-				if ("application/mercurial-0.1".equals(mimetype)) {
-					return new ContentHandler() {
-						
-						@Override
-						public Object getContent(URLConnection urlc) throws IOException {
-							if (urlc.getContentLength() > 0) {
-								ByteArrayOutputStream bos = new ByteArrayOutputStream();
-								InputStream is = urlc.getInputStream();
-								int r;
-								while ((r = is.read()) != -1) {
-									bos.write(r);
-								}
-								return new String(bos.toByteArray());
-							}
-							return "<empty>";
-						}
-					};
-				}
-				return null;
-			}
-		});
-	}
-	
-	HgRemoteRepository(SessionContext ctx, URL url) throws HgBadArgumentException {
-		if (url == null || ctx == null) {
-			throw new IllegalArgumentException();
+	HgRemoteRepository(SessionContext ctx, RemoteDescriptor rd) throws HgBadArgumentException {
+		if (false == rd instanceof RemoteConnectorDescriptor) {
+			throw new IllegalArgumentException(String.format("Present implementation supports remote connections via %s only", Connector.class.getName()));
 		}
 		sessionContext = ctx;
 		debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false);
-		remote = "ssh".equals(url.getProtocol()) ? new SshConnector() : new HttpConnector();
-		remote.init(url, ctx, null);
+		remote = ((RemoteConnectorDescriptor) rd).createConnector();
+		remote.init(rd.getURI(), ctx, null);
 	}
 	
 	public boolean isInvalid() throws HgRemoteConnectionException {
@@ -518,6 +486,19 @@
 			// in fact, p1 and p2 are not supposed to be null, ever (at least for RemoteBranch created from server output)
 			return head.equals(o.head) && root.equals(o.root) && (p1 == null && o.p1 == null || p1.equals(o.p1)) && (p2 == null && o.p2 == null || p2.equals(o.p2));
 		}
+		
+		@Override
+		public int hashCode() {
+			return head.hashCode() ^ root.hashCode();
+		}
+		
+		@Override
+		public String toString() {
+			String none = String.valueOf(-1);
+			String s1 = p1 == null || p1.isNull() ? none : p1.shortNotation();
+			String s2 = p2 == null || p2.isNull() ? none : p2.shortNotation();
+			return String.format("RemoteBranch[root: %s, head:%s, p1:%s, p2:%s]", root.shortNotation(), head.shortNotation(), s1, s2);
+		}
 	}
 
 	public static final class Bookmarks implements Iterable<Pair<String, Nodeid>> {
--- a/test/org/tmatesoft/hg/test/TestIncoming.java	Tue Aug 06 13:34:34 2013 +0200
+++ b/test/org/tmatesoft/hg/test/TestIncoming.java	Tue Aug 06 21:18:33 2013 +0200
@@ -52,6 +52,8 @@
 	}
 
 	public TestIncoming() {
+//		Configuration.get().remoteServers("http://hg.serpentine.com/tutorial/hello/");
+//		Configuration.get().remoteServers("ssh://localhost/hg/hgtest-annotate-merge/");
 //		Configuration.get().remoteServers("http://localhost:8000/");
 	}