]> source.dussan.org Git - jgit.git/commitdiff
Make the supported Transports extensible and discoverable 66/2666/2
authorShawn O. Pearce <spearce@spearce.org>
Mon, 7 Mar 2011 23:01:49 +0000 (15:01 -0800)
committerShawn O. Pearce <spearce@spearce.org>
Sun, 13 Mar 2011 23:23:56 +0000 (16:23 -0700)
The new TransportProtocol type describes what a particular Transport
implementation wants in order to support a connection.  3rd parties
can now plug into the Transport.open() logic by implementing their
own TransportProtocol and Transport classes, and registering with
Transport.register().

GUI applications can help the user configure a connection by looking
at the supported fields of a particular TransportProtocol type, which
makes the GUI more dynamic and may better support new Transports.

Change-Id: Iafd8e3a6285261412aac6cba8e2c333f8b7b76a5
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
15 files changed:
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackRefFilterTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRemoteException.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java

index 5fd76d4add80f131b8aceb05af2b75052bc5121d..c58252a230893ded3af10986f73e56cee1316bd4 100644 (file)
@@ -143,7 +143,7 @@ public class ReceivePackRefFilterTest extends LocalDiskRepositoryTestCase {
        @Test
        public void testFilterHidesPrivate() throws Exception {
                Map<String, Ref> refs;
-               TransportLocal t = new TransportLocal(src, uriOf(dst)) {
+               TransportLocal t = new TransportLocal(src, uriOf(dst), dst.getDirectory()) {
                        @Override
                        ReceivePack createReceivePack(final Repository db) {
                                db.close();
@@ -206,7 +206,7 @@ public class ReceivePackRefFilterTest extends LocalDiskRepositoryTestCase {
 
                // Push this new content to the remote, doing strict validation.
                //
-               TransportLocal t = new TransportLocal(src, uriOf(dst)) {
+               TransportLocal t = new TransportLocal(src, uriOf(dst), dst.getDirectory()) {
                        @Override
                        ReceivePack createReceivePack(final Repository db) {
                                db.close();
index 34dadf39eb9ebcc319b63123bc0bd625ac9702d9..bdc9b3ecd60c45899e3287512c038154179e8bfe 100644 (file)
@@ -549,6 +549,6 @@ public class URIishTest {
        public void testMissingPort() throws URISyntaxException {
                final String incorrectSshUrl = "ssh://some-host:/path/to/repository.git";
                URIish u = new URIish(incorrectSshUrl);
-               assertFalse(TransportGitSsh.canHandle(u));
+               assertFalse(TransportGitSsh.PROTO_SSH.canHandle(null, u, null));
        }
 }
index f96d4261ccbb0f6a6a5e948f4f6eea174c408f2b..9fe17fef00a2dc966f142bb9628bf3e18828c508 100644 (file)
@@ -419,6 +419,14 @@ transportExceptionEmptyRef=Empty ref: {0}
 transportExceptionInvalid=Invalid {0} {1}:{2}
 transportExceptionMissingAssumed=Missing assumed {0}
 transportExceptionReadRef=read {0}
+transportProtoAmazonS3=Amazon S3
+transportProtoBundleFile=Git Bundle File
+transportProtoGitAnon=Anonymous Git
+transportProtoFTP=FTP
+transportProtoHTTP=HTTP
+transportProtoLocal=Local Git Repository
+transportProtoSFTP=SFTP
+transportProtoSSH=SSH
 treeEntryAlreadyExists=Tree entry "{0}" already exists.
 treeIteratorDoesNotSupportRemove=TreeIterator does not support remove()
 truncatedHunkLinesMissingForAncestor=Truncated hunk, at least {0} lines missing for ancestor {1}
index 9ef2cd93ee13876fefc88da3401d2d13c2974f87..e07a4252eed1660106cc543251710b997d709ce1 100644 (file)
@@ -479,6 +479,14 @@ public class JGitText extends TranslationBundle {
        /***/ public String transportExceptionInvalid;
        /***/ public String transportExceptionMissingAssumed;
        /***/ public String transportExceptionReadRef;
+       /***/ public String transportProtoAmazonS3;
+       /***/ public String transportProtoBundleFile;
+       /***/ public String transportProtoFTP;
+       /***/ public String transportProtoGitAnon;
+       /***/ public String transportProtoHTTP;
+       /***/ public String transportProtoLocal;
+       /***/ public String transportProtoSFTP;
+       /***/ public String transportProtoSSH;
        /***/ public String treeEntryAlreadyExists;
        /***/ public String treeIteratorDoesNotSupportRemove;
        /***/ public String truncatedHunkLinesMissingForAncestor;
index 9594dfebe7def7be192f220f7f6dede2001b5d70..51dc5295fa7c6befebab4329feb1313ac3261584 100644 (file)
@@ -50,6 +50,7 @@ import java.util.List;
 import org.eclipse.jgit.JGitText;
 import org.eclipse.jgit.api.errors.InvalidRemoteException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.errors.NoRemoteRepositoryException;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.Constants;
@@ -121,27 +122,29 @@ public class FetchCommand extends GitCommand<FetchResult> {
 
                try {
                        Transport transport = Transport.open(repo, remote);
-                       transport.setCheckFetchedObjects(checkFetchedObjects);
-                       transport.setRemoveDeletedRefs(removeDeletedRefs);
-                       transport.setTimeout(timeout);
-                       transport.setDryRun(dryRun);
-                       if (tagOption != null)
-                               transport.setTagOpt(tagOption);
-                       transport.setFetchThin(thin);
-                       if (credentialsProvider != null)
-                               transport.setCredentialsProvider(credentialsProvider);
-
                        try {
+                               transport.setCheckFetchedObjects(checkFetchedObjects);
+                               transport.setRemoveDeletedRefs(removeDeletedRefs);
+                               transport.setTimeout(timeout);
+                               transport.setDryRun(dryRun);
+                               if (tagOption != null)
+                                       transport.setTagOpt(tagOption);
+                               transport.setFetchThin(thin);
+                               if (credentialsProvider != null)
+                                       transport.setCredentialsProvider(credentialsProvider);
+
                                FetchResult result = transport.fetch(monitor, refSpecs);
                                return result;
-
-                       } catch (TransportException e) {
-                               throw new JGitInternalException(
-                                               JGitText.get().exceptionCaughtDuringExecutionOfFetchCommand,
-                                               e);
                        } finally {
                                transport.close();
                        }
+               } catch (NoRemoteRepositoryException e) {
+                       throw new InvalidRemoteException(MessageFormat.format(
+                                       JGitText.get().invalidRemote, remote), e);
+               } catch (TransportException e) {
+                       throw new JGitInternalException(
+                                       JGitText.get().exceptionCaughtDuringExecutionOfFetchCommand,
+                                       e);
                } catch (URISyntaxException e) {
                        throw new InvalidRemoteException(MessageFormat.format(
                                        JGitText.get().invalidRemote, remote));
index 4104fd669b9644d8164656dae40f6186da59a235..3f059b79c79acdb0be1707f46db6a33f2f93f868 100644 (file)
@@ -44,9 +44,17 @@ public class InvalidRemoteException extends GitAPIException {
        private static final long serialVersionUID = 1L;
 
        /**
-        * @param msg
+        * @param msg message describing the invalid remote.
         */
        public InvalidRemoteException(String msg) {
                super(msg);
        }
+
+       /**
+        * @param msg message describing the invalid remote.
+        * @param cause why the remote is invalid.
+        */
+       public InvalidRemoteException(String msg, Throwable cause) {
+               super(msg, cause);
+       }
 }
index 497fb844bf0d44e46ab256fc73851cdc89a373f6..073b1ac1af276b25123debc1eb7618bc8bc3f3f6 100644 (file)
@@ -47,6 +47,7 @@
 package org.eclipse.jgit.transport;
 
 import java.io.IOException;
+import java.lang.ref.WeakReference;
 import java.net.URISyntaxException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
@@ -56,6 +57,7 @@ import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.eclipse.jgit.JGitText;
 import org.eclipse.jgit.errors.NotSupportedException;
@@ -66,7 +68,6 @@ import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.storage.pack.PackConfig;
-import org.eclipse.jgit.util.FS;
 
 /**
  * Connects two Git repositories together and copies objects between them.
@@ -90,6 +91,79 @@ public abstract class Transport {
                PUSH;
        }
 
+       private static final List<WeakReference<TransportProtocol>> protocols =
+               new CopyOnWriteArrayList<WeakReference<TransportProtocol>>();
+
+       static {
+               // Registration goes backwards in order of priority.
+               register(TransportLocal.PROTO_LOCAL);
+               register(TransportBundleFile.PROTO_BUNDLE);
+               register(TransportAmazonS3.PROTO_S3);
+               register(TransportGitAnon.PROTO_GIT);
+               register(TransportSftp.PROTO_SFTP);
+               register(TransportHttp.PROTO_FTP);
+               register(TransportHttp.PROTO_HTTP);
+               register(TransportGitSsh.PROTO_SSH);
+       }
+
+       /**
+        * Register a TransportProtocol instance for use during open.
+        * <p>
+        * Protocol definitions are held by WeakReference, allowing them to be
+        * garbage collected when the calling application drops all strongly held
+        * references to the TransportProtocol. Therefore applications should use a
+        * singleton pattern as described in {@link TransportProtocol}'s class
+        * documentation to ensure their protocol does not get disabled by garbage
+        * collection earlier than expected.
+        * <p>
+        * The new protocol is registered in front of all earlier protocols, giving
+        * it higher priority than the built-in protocol definitions.
+        *
+        * @param proto
+        *            the protocol definition. Must not be null.
+        */
+       public static void register(TransportProtocol proto) {
+               protocols.add(0, new WeakReference<TransportProtocol>(proto));
+       }
+
+       /**
+        * Unregister a TransportProtocol instance.
+        * <p>
+        * Unregistering a protocol usually isn't necessary, as protocols are held
+        * by weak references and will automatically clear when they are garbage
+        * collected by the JVM. Matching is handled by reference equality, so the
+        * exact reference given to {@link #register(TransportProtocol)} must be
+        * used.
+        *
+        * @param proto
+        *            the exact object previously given to register.
+        */
+       public static void unregister(TransportProtocol proto) {
+               for (WeakReference<TransportProtocol> ref : protocols) {
+                       TransportProtocol refProto = ref.get();
+                       if (refProto == null || refProto == proto)
+                               protocols.remove(ref);
+               }
+       }
+
+       /**
+        * Obtain a copy of the registered protocols.
+        *
+        * @return an immutable copy of the currently registered protocols.
+        */
+       public static List<TransportProtocol> getTransportProtocols() {
+               int cnt = protocols.size();
+               List<TransportProtocol> res = new ArrayList<TransportProtocol>(cnt);
+               for (WeakReference<TransportProtocol> ref : protocols) {
+                       TransportProtocol proto = ref.get();
+                       if (proto != null)
+                               res.add(proto);
+                       else
+                               protocols.remove(ref);
+               }
+               return Collections.unmodifiableList(res);
+       }
+
        /**
         * Open a new transport instance to connect two repositories.
         * <p>
@@ -107,9 +181,12 @@ public abstract class Transport {
         *             file and is not a well-formed URL.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         */
        public static Transport open(final Repository local, final String remote)
-                       throws NotSupportedException, URISyntaxException {
+                       throws NotSupportedException, URISyntaxException,
+                       TransportException {
                return open(local, remote, Operation.FETCH);
        }
 
@@ -131,13 +208,15 @@ public abstract class Transport {
         *             file and is not a well-formed URL.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         */
        public static Transport open(final Repository local, final String remote,
                        final Operation op) throws NotSupportedException,
-                       URISyntaxException {
+                       URISyntaxException, TransportException {
                final RemoteConfig cfg = new RemoteConfig(local.getConfig(), remote);
                if (doesNotExist(cfg))
-                       return open(local, new URIish(remote));
+                       return open(local, new URIish(remote), null);
                return open(local, cfg, op);
        }
 
@@ -158,10 +237,12 @@ public abstract class Transport {
         *             file and is not a well-formed URL.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         */
        public static List<Transport> openAll(final Repository local,
                        final String remote) throws NotSupportedException,
-                       URISyntaxException {
+                       URISyntaxException, TransportException {
                return openAll(local, remote, Operation.FETCH);
        }
 
@@ -183,14 +264,17 @@ public abstract class Transport {
         *             file and is not a well-formed URL.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         */
        public static List<Transport> openAll(final Repository local,
                        final String remote, final Operation op)
-                       throws NotSupportedException, URISyntaxException {
+                       throws NotSupportedException, URISyntaxException,
+                       TransportException {
                final RemoteConfig cfg = new RemoteConfig(local.getConfig(), remote);
                if (doesNotExist(cfg)) {
                        final ArrayList<Transport> transports = new ArrayList<Transport>(1);
-                       transports.add(open(local, new URIish(remote)));
+                       transports.add(open(local, new URIish(remote), null));
                        return transports;
                }
                return openAll(local, cfg, op);
@@ -210,12 +294,14 @@ public abstract class Transport {
         *         in remote configuration, only the first is chosen.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         * @throws IllegalArgumentException
         *             if provided remote configuration doesn't have any URI
         *             associated.
         */
        public static Transport open(final Repository local, final RemoteConfig cfg)
-                       throws NotSupportedException {
+                       throws NotSupportedException, TransportException {
                return open(local, cfg, Operation.FETCH);
        }
 
@@ -234,18 +320,20 @@ public abstract class Transport {
         *         in remote configuration, only the first is chosen.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         * @throws IllegalArgumentException
         *             if provided remote configuration doesn't have any URI
         *             associated.
         */
        public static Transport open(final Repository local,
                        final RemoteConfig cfg, final Operation op)
-                       throws NotSupportedException {
+                       throws NotSupportedException, TransportException {
                final List<URIish> uris = getURIs(cfg, op);
                if (uris.isEmpty())
                        throw new IllegalArgumentException(MessageFormat.format(
                                        JGitText.get().remoteConfigHasNoURIAssociated, cfg.getName()));
-               final Transport tn = open(local, uris.get(0));
+               final Transport tn = open(local, uris.get(0), cfg.getName());
                tn.applyConfig(cfg);
                return tn;
        }
@@ -264,9 +352,12 @@ public abstract class Transport {
         *         configuration.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         */
        public static List<Transport> openAll(final Repository local,
-                       final RemoteConfig cfg) throws NotSupportedException {
+                       final RemoteConfig cfg) throws NotSupportedException,
+                       TransportException {
                return openAll(local, cfg, Operation.FETCH);
        }
 
@@ -285,14 +376,16 @@ public abstract class Transport {
         *         configuration.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         */
        public static List<Transport> openAll(final Repository local,
                        final RemoteConfig cfg, final Operation op)
-                       throws NotSupportedException {
+                       throws NotSupportedException, TransportException {
                final List<URIish> uris = getURIs(cfg, op);
                final List<Transport> transports = new ArrayList<Transport>(uris.size());
                for (final URIish uri : uris) {
-                       final Transport tn = open(local, uri);
+                       final Transport tn = open(local, uri, cfg.getName());
                        tn.applyConfig(cfg);
                        transports.add(tn);
                }
@@ -320,37 +413,21 @@ public abstract class Transport {
        }
 
        /**
-        * Determines whether the transport can handle the given URIish.
+        * Open a new transport instance to connect two repositories.
         *
-        * @param remote
+        * @param local
+        *            existing local repository.
+        * @param uri
         *            location of the remote repository.
-        * @param fs
-        *            type of filesystem the local repository is stored on.
-        * @return true if the protocol is supported.
+        * @return the new transport instance. Never null.
+        * @throws NotSupportedException
+        *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         */
-       public static boolean canHandleProtocol(final URIish remote, final FS fs) {
-               if (TransportGitSsh.canHandle(remote))
-                       return true;
-
-               else if (TransportHttp.canHandle(remote))
-                       return true;
-
-               else if (TransportSftp.canHandle(remote))
-                       return true;
-
-               else if (TransportGitAnon.canHandle(remote))
-                       return true;
-
-               else if (TransportAmazonS3.canHandle(remote))
-                       return true;
-
-               else if (TransportBundleFile.canHandle(remote, fs))
-                       return true;
-
-               else if (TransportLocal.canHandle(remote, fs))
-                       return true;
-
-               return false;
+       public static Transport open(final Repository local, final URIish uri)
+                       throws NotSupportedException, TransportException {
+               return open(local, uri, null);
        }
 
        /**
@@ -358,36 +435,31 @@ public abstract class Transport {
         *
         * @param local
         *            existing local repository.
-        * @param remote
+        * @param uri
         *            location of the remote repository.
+        * @param remoteName
+        *            name of the remote, if the remote as configured in
+        *            {@code local}; otherwise null.
         * @return the new transport instance. Never null.
         * @throws NotSupportedException
         *             the protocol specified is not supported.
+        * @throws TransportException
+        *             the transport cannot open this URI.
         */
-       public static Transport open(final Repository local, final URIish remote)
-                       throws NotSupportedException {
-               if (TransportGitSsh.canHandle(remote))
-                       return new TransportGitSsh(local, remote);
-
-               else if (TransportHttp.canHandle(remote))
-                       return new TransportHttp(local, remote);
-
-               else if (TransportSftp.canHandle(remote))
-                       return new TransportSftp(local, remote);
-
-               else if (TransportGitAnon.canHandle(remote))
-                       return new TransportGitAnon(local, remote);
-
-               else if (TransportAmazonS3.canHandle(remote))
-                       return new TransportAmazonS3(local, remote);
-
-               else if (TransportBundleFile.canHandle(remote, local.getFS()))
-                       return new TransportBundleFile(local, remote);
+       public static Transport open(Repository local, URIish uri, String remoteName)
+                       throws NotSupportedException, TransportException {
+               for (WeakReference<TransportProtocol> ref : protocols) {
+                       TransportProtocol proto = ref.get();
+                       if (proto == null) {
+                               protocols.remove(ref);
+                               continue;
+                       }
 
-               else if (TransportLocal.canHandle(remote, local.getFS()))
-                       return new TransportLocal(local, remote);
+                       if (proto.canHandle(local, uri, remoteName))
+                               return proto.open(local, uri, remoteName);
+               }
 
-               throw new NotSupportedException(MessageFormat.format(JGitText.get().URINotSupported, remote));
+               throw new NotSupportedException(MessageFormat.format(JGitText.get().URINotSupported, uri));
        }
 
        /**
index 79b88b6a73cca9a16b69d26b9983fd2317f2fe83..0f65e0cac8e65d3d721023d694601a0dce39be2d 100644 (file)
@@ -53,9 +53,12 @@ import java.net.URLConnection;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.TreeMap;
 
 import org.eclipse.jgit.JGitText;
@@ -66,9 +69,9 @@ import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Ref.Storage;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.lib.Ref.Storage;
 
 /**
  * Transport over the non-Git aware Amazon S3 protocol.
@@ -97,11 +100,29 @@ import org.eclipse.jgit.lib.Ref.Storage;
 public class TransportAmazonS3 extends HttpTransport implements WalkTransport {
        static final String S3_SCHEME = "amazon-s3";
 
-       static boolean canHandle(final URIish uri) {
-               if (!uri.isRemote())
-                       return false;
-               return S3_SCHEME.equals(uri.getScheme());
-       }
+       static final TransportProtocol PROTO_S3 = new TransportProtocol() {
+               public String getName() {
+                       return "Amazon S3";
+               }
+
+               public Set<String> getSchemes() {
+                       return Collections.singleton(S3_SCHEME);
+               }
+
+               public Set<URIishField> getRequiredFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
+                                       URIishField.HOST, URIishField.PATH));
+               }
+
+               public Set<URIishField> getOptionalFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.PASS));
+               }
+
+               public Transport open(Repository local, URIish uri, String remoteName)
+                               throws NotSupportedException {
+                       return new TransportAmazonS3(local, uri);
+               }
+       };
 
        /** User information necessary to connect to S3. */
        private final AmazonS3 s3;
index c47833f21cf08715f3011b81c575e8a1ec79f912..15aa1fff0510c01bb7817a9f7382ae10811d377c 100644 (file)
@@ -50,32 +50,67 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
 
 import org.eclipse.jgit.JGitText;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.util.FS;
 
 class TransportBundleFile extends Transport implements TransportBundle {
-       static boolean canHandle(final URIish uri, FS fs) {
-               if (uri.getHost() != null || uri.getPort() > 0 || uri.getUser() != null
-                               || uri.getPass() != null || uri.getPath() == null)
-                       return false;
-
-               if ("file".equals(uri.getScheme()) || uri.getScheme() == null) {
-                       final File f = fs.resolve(new File("."), uri.getPath());
-                       return f.isFile() || f.getName().endsWith(".bundle");
+       static final TransportProtocol PROTO_BUNDLE = new TransportProtocol() {
+               private final String[] schemeNames = { "bundle", "file" }; //$NON-NLS-1$ //$NON-NLS-2$
+
+               private final Set<String> schemeSet = Collections
+                               .unmodifiableSet(new LinkedHashSet<String>(Arrays
+                                               .asList(schemeNames)));
+
+               @Override
+               public String getName() {
+                       return JGitText.get().transportProtoBundleFile;
                }
 
-               return false;
-       }
+               public Set<String> getSchemes() {
+                       return schemeSet;
+               }
+
+               @Override
+               public boolean canHandle(Repository local, URIish uri, String remoteName) {
+                       if (uri.getPath() == null
+                                       || uri.getPort() > 0
+                                       || uri.getUser() != null
+                                       || uri.getPass() != null
+                                       || uri.getHost() != null
+                                       || (uri.getScheme() != null && !getSchemes().contains(uri.getScheme())))
+                               return false;
+                       return true;
+               }
+
+               @Override
+               public Transport open(Repository local, URIish uri, String remoteName)
+                               throws NotSupportedException, TransportException {
+                       if ("bundle".equals(uri.getScheme())) {
+                               File path = local.getFS().resolve(new File("."), uri.getPath());
+                               return new TransportBundleFile(local, uri, path);
+                       }
+
+                       // This is an ambiguous reference, it could be a bundle file
+                       // or it could be a Git repository. Allow TransportLocal to
+                       // resolve the path and figure out which type it is by testing
+                       // the target.
+                       //
+                       return TransportLocal.PROTO_LOCAL.open(local, uri, remoteName);
+               }
+       };
 
        private final File bundle;
 
-       TransportBundleFile(final Repository local, final URIish uri) {
+       TransportBundleFile(Repository local, URIish uri, File bundlePath) {
                super(local, uri);
-               bundle = local.getFS().resolve(new File("."), uri.getPath()).getAbsoluteFile();
+               bundle = bundlePath;
        }
 
        @Override
index 5ad57768fe2f019b41c10a84663e460bb2e54ce2..081e7a90b7c893178dd0d58b89a89e6b986dcce5 100644 (file)
@@ -55,8 +55,12 @@ import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
 
 import org.eclipse.jgit.JGitText;
+import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.Repository;
 
@@ -70,9 +74,33 @@ import org.eclipse.jgit.lib.Repository;
 class TransportGitAnon extends TcpTransport implements PackTransport {
        static final int GIT_PORT = Daemon.DEFAULT_PORT;
 
-       static boolean canHandle(final URIish uri) {
-               return "git".equals(uri.getScheme());
-       }
+       static final TransportProtocol PROTO_GIT = new TransportProtocol() {
+               public String getName() {
+                       return JGitText.get().transportProtoGitAnon;
+               }
+
+               public Set<String> getSchemes() {
+                       return Collections.singleton("git"); //$NON-NLS-1$
+               }
+
+               public Set<URIishField> getRequiredFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
+                                       URIishField.PATH));
+               }
+
+               public Set<URIishField> getOptionalFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.PORT));
+               }
+
+               public int getDefaultPort() {
+                       return GIT_PORT;
+               }
+
+               public Transport open(Repository local, URIish uri, String remoteName)
+                               throws NotSupportedException {
+                       return new TransportGitAnon(local, uri);
+               }
+       };
 
        TransportGitAnon(final Repository local, final URIish uri) {
                super(local, uri);
index 7ad5fc71c12f61fbea30476c4e42d8c2b060d22d..a0cb8a3804a48edc690c57666d48486265ade369 100644 (file)
@@ -53,10 +53,16 @@ import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.eclipse.jgit.JGitText;
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
+import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
@@ -80,20 +86,52 @@ import com.jcraft.jsch.JSchException;
  * enumeration, save file modification and hook execution.
  */
 public class TransportGitSsh extends SshTransport implements PackTransport {
-       static boolean canHandle(final URIish uri) {
-               if (!uri.isRemote())
-                       return false;
-               final String scheme = uri.getScheme();
-               if ("ssh".equals(scheme))
-                       return true;
-               if ("ssh+git".equals(scheme))
-                       return true;
-               if ("git+ssh".equals(scheme))
-                       return true;
-               if (scheme == null && uri.getHost() != null && uri.getPath() != null)
-                       return true;
-               return false;
-       }
+       static final TransportProtocol PROTO_SSH = new TransportProtocol() {
+               private final String[] schemeNames = { "ssh", "ssh+git", "git+ssh" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+
+               private final Set<String> schemeSet = Collections
+                               .unmodifiableSet(new LinkedHashSet<String>(Arrays
+                                               .asList(schemeNames)));
+
+               public String getName() {
+                       return JGitText.get().transportProtoSSH;
+               }
+
+               public Set<String> getSchemes() {
+                       return schemeSet;
+               }
+
+               public Set<URIishField> getRequiredFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
+                                       URIishField.PATH));
+               }
+
+               public Set<URIishField> getOptionalFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
+                                       URIishField.PASS, URIishField.PORT));
+               }
+
+               public int getDefaultPort() {
+                       return 22;
+               }
+
+               @Override
+               public boolean canHandle(Repository local, URIish uri, String remoteName) {
+                       if (uri.getScheme() == null) {
+                               // scp-style URI "host:path" does not have scheme.
+                               return uri.getHost() != null
+                                       && uri.getPath() != null
+                                       && uri.getHost().length() != 0
+                                       && uri.getPath().length() != 0;
+                       }
+                       return super.canHandle(local, uri, remoteName);
+               }
+
+               public Transport open(Repository local, URIish uri, String remoteName)
+                               throws NotSupportedException {
+                       return new TransportGitSsh(local, uri);
+               }
+       };
 
        TransportGitSsh(final Repository local, final URIish uri) {
                super(local, uri);
index 5c4e11036c1d4057539eec1ac558184360d46f50..bb69b8c078a8c1e334dceb38fe190cda25a04321 100644 (file)
@@ -72,7 +72,11 @@ import java.security.NoSuchAlgorithmException;
 import java.security.cert.X509Certificate;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
@@ -130,12 +134,69 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
        private static final String userAgent = computeUserAgent();
 
-       static boolean canHandle(final URIish uri) {
-               if (!uri.isRemote())
-                       return false;
-               final String s = uri.getScheme();
-               return "http".equals(s) || "https".equals(s) || "ftp".equals(s); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
-       }
+       static final TransportProtocol PROTO_HTTP = new TransportProtocol() {
+               private final String[] schemeNames = { "http", "https" }; //$NON-NLS-1$ //$NON-NLS-2$
+
+               private final Set<String> schemeSet = Collections
+                               .unmodifiableSet(new LinkedHashSet<String>(Arrays
+                                               .asList(schemeNames)));
+
+               public String getName() {
+                       return JGitText.get().transportProtoHTTP;
+               }
+
+               public Set<String> getSchemes() {
+                       return schemeSet;
+               }
+
+               public Set<URIishField> getRequiredFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
+                                       URIishField.PATH));
+               }
+
+               public Set<URIishField> getOptionalFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
+                                       URIishField.PASS, URIishField.PORT));
+               }
+
+               public int getDefaultPort() {
+                       return 80;
+               }
+
+               public Transport open(Repository local, URIish uri, String remoteName)
+                               throws NotSupportedException {
+                       return new TransportHttp(local, uri);
+               }
+       };
+
+       static final TransportProtocol PROTO_FTP = new TransportProtocol() {
+               public String getName() {
+                       return JGitText.get().transportProtoFTP;
+               }
+
+               public Set<String> getSchemes() {
+                       return Collections.singleton("ftp"); //$NON-NLS-1$
+               }
+
+               public Set<URIishField> getRequiredFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
+                                       URIishField.PATH));
+               }
+
+               public Set<URIishField> getOptionalFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
+                                       URIishField.PASS, URIishField.PORT));
+               }
+
+               public int getDefaultPort() {
+                       return 21;
+               }
+
+               public Transport open(Repository local, URIish uri, String remoteName)
+                               throws NotSupportedException {
+                       return new TransportHttp(local, uri);
+               }
+       };
 
        private static String computeUserAgent() {
                String version;
index 3ede748b340d2b9c5fb3cc300d9fe83afb56a7e0..280b7c58eca83061232a5fa94c4bafd69cb1b353 100644 (file)
@@ -55,15 +55,17 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.PipedInputStream;
 import java.io.PipedOutputStream;
+import java.util.Collections;
 import java.util.Map;
+import java.util.Set;
 
 import org.eclipse.jgit.JGitText;
+import org.eclipse.jgit.errors.NoRemoteRepositoryException;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.storage.file.FileRepository;
-import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.io.MessageWriter;
 import org.eclipse.jgit.util.io.StreamCopyThread;
 
@@ -91,27 +93,50 @@ import org.eclipse.jgit.util.io.StreamCopyThread;
  * system pipe to transfer data.
  */
 class TransportLocal extends Transport implements PackTransport {
-       private static final String PWD = ".";
+       static final TransportProtocol PROTO_LOCAL = new TransportProtocol() {
+               @Override
+               public String getName() {
+                       return JGitText.get().transportProtoLocal;
+               }
 
-       static boolean canHandle(final URIish uri, FS fs) {
-               if (uri.getHost() != null || uri.getPort() > 0 || uri.getUser() != null
-                               || uri.getPass() != null || uri.getPath() == null)
-                       return false;
+               public Set<String> getSchemes() {
+                       return Collections.singleton("file"); //$NON-NLS-1$
+               }
 
-               if ("file".equals(uri.getScheme()) || uri.getScheme() == null)
-                       return fs.resolve(new File(PWD), uri.getPath()).isDirectory();
-               return false;
-       }
+               @Override
+               public boolean canHandle(Repository local, URIish uri, String remoteName) {
+                       if (uri.getPath() == null
+                                       || uri.getPort() > 0
+                                       || uri.getUser() != null
+                                       || uri.getPass() != null
+                                       || uri.getHost() != null
+                                       || (uri.getScheme() != null && !getSchemes().contains(uri.getScheme())))
+                               return false;
+                       return true;
+               }
+
+               @Override
+               public Transport open(Repository local, URIish uri, String remoteName)
+                               throws NoRemoteRepositoryException {
+                       // If the reference is to a local file, C Git behavior says
+                       // assume this is a bundle, since repositories are directories.
+                       //
+                       File path = local.getFS().resolve(new File("."), uri.getPath());
+                       if (path.isFile())
+                               return new TransportBundleFile(local, uri, path);
+
+                       File gitDir = RepositoryCache.FileKey.resolve(path, local.getFS());
+                       if (gitDir == null)
+                               throw new NoRemoteRepositoryException(uri, JGitText.get().notFound);
+                       return new TransportLocal(local, uri, gitDir);
+               }
+       };
 
        private final File remoteGitDir;
 
-       TransportLocal(final Repository local, final URIish uri) {
+       TransportLocal(Repository local, URIish uri, File gitDir) {
                super(local, uri);
-
-               File d = local.getFS().resolve(new File(PWD), uri.getPath()).getAbsoluteFile();
-               if (new File(d, Constants.DOT_GIT).isDirectory())
-                       d = new File(d, Constants.DOT_GIT);
-               remoteGitDir = d;
+               remoteGitDir = gitDir;
        }
 
        UploadPack createUploadPack(final Repository dst) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java
new file mode 100644 (file)
index 0000000..4652e01
--- /dev/null
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2011, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Describes a way to connect to another Git repository.
+ * <p>
+ * Implementations of this class are typically immutable singletons held by
+ * static class members, for example:
+ *
+ * <pre>
+ * class MyTransport extends Transport {
+ *     static final TransportProtocol PROTO = new TransportProtocol() {
+ *             public String getName() {
+ *                     return &quot;My Protocol&quot;;
+ *             }
+ *     };
+ * }
+ * </pre>
+ *
+ * Applications may register additional protocols for use by JGit by calling
+ * {@link Transport#register(TransportProtocol)}. Because that API holds onto
+ * the protocol object by a WeakReference, applications must ensure their own
+ * ClassLoader retains the TransportProtocol for the life of the application.
+ * Using a static singleton pattern as above will ensure the protocol is valid
+ * so long as the ClassLoader that defines it remains valid.
+ */
+public abstract class TransportProtocol {
+       /** Fields within a {@link URIish} that a transport uses. */
+       public static enum URIishField {
+               /** the user field */
+               USER,
+               /** the pass (aka password) field */
+               PASS,
+               /** the host field */
+               HOST,
+               /** the port field */
+               PORT,
+               /** the path field */
+               PATH,
+       }
+
+       /** @return text name of the protocol suitable for display to a user. */
+       public abstract String getName();
+
+       /** @return immutable set of schemes supported by this protocol. */
+       public Set<String> getSchemes() {
+               return Collections.emptySet();
+       }
+
+       /** @return immutable set of URIishFields that must be filled in. */
+       public Set<URIishField> getRequiredFields() {
+               return Collections.unmodifiableSet(EnumSet.of(URIishField.PATH));
+       }
+
+       /** @return immutable set of URIishFields that may be filled in. */
+       public Set<URIishField> getOptionalFields() {
+               return Collections.emptySet();
+       }
+
+       /** @return if a port is supported, the default port, else -1. */
+       public int getDefaultPort() {
+               return -1;
+       }
+
+       /**
+        * Determine if this protocol can handle a particular URI.
+        * <p>
+        * Implementations should try to avoid looking at the local filesystem, but
+        * may look at implementation specific configuration options in the remote
+        * block of {@code local.getConfig()} using {@code remoteName} if the name
+        * is non-null.
+        * <p>
+        * The default implementation of this method matches the scheme against
+        * {@link #getSchemes()}, required fields against
+        * {@link #getRequiredFields()}, and optional fields against
+        * {@link #getOptionalFields()}, returning true only if all of the fields
+        * match the specification.
+        *
+        * @param local
+        *            the local repository that will communicate with the other Git
+        *            repository.
+        * @param uri
+        *            address of the Git repository; never null.
+        * @param remoteName
+        *            name of the remote, if the remote as configured in
+        *            {@code local}; otherwise null.
+        * @return true if this protocol can handle this URI; false otherwise.
+        */
+       public boolean canHandle(Repository local, URIish uri, String remoteName) {
+               if (!getSchemes().isEmpty() && !getSchemes().contains(uri.getScheme()))
+                       return false;
+
+               for (URIishField field : getRequiredFields()) {
+                       switch (field) {
+                       case USER:
+                               if (uri.getUser() == null || uri.getUser().length() == 0)
+                                       return false;
+                               break;
+
+                       case PASS:
+                               if (uri.getPass() == null || uri.getPass().length() == 0)
+                                       return false;
+                               break;
+
+                       case HOST:
+                               if (uri.getHost() == null || uri.getHost().length() == 0)
+                                       return false;
+                               break;
+
+                       case PORT:
+                               if (uri.getPort() <= 0)
+                                       return false;
+                               break;
+
+                       case PATH:
+                               if (uri.getPath() == null || uri.getPath().length() == 0)
+                                       return false;
+                               break;
+
+                       default:
+                               return false;
+                       }
+               }
+
+               Set<URIishField> canHave = EnumSet.copyOf(getRequiredFields());
+               canHave.addAll(getOptionalFields());
+
+               if (uri.getUser() != null && !canHave.contains(URIishField.USER))
+                       return false;
+               if (uri.getPass() != null && !canHave.contains(URIishField.PASS))
+                       return false;
+               if (uri.getHost() != null && !canHave.contains(URIishField.HOST))
+                       return false;
+               if (uri.getPort() > 0 && !canHave.contains(URIishField.PORT))
+                       return false;
+               if (uri.getPath() != null && !canHave.contains(URIishField.PATH))
+                       return false;
+
+               return true;
+       }
+
+       /**
+        * Open a Transport instance to the other repository.
+        * <p>
+        * Implementations should avoid making remote connections until an operation
+        * on the returned Transport is invoked, however they may fail fast here if
+        * they know a connection is impossible, such as when using the local
+        * filesystem and the target path does not exist.
+        * <p>
+        * Implementations may access implementation-specific configuration options
+        * within {@code local.getConfig()} using the remote block named by the
+        * {@code remoteName}, if the name is non-null.
+        *
+        * @param local
+        *            the local repository that will communicate with the other Git
+        *            repository.
+        * @param uri
+        *            address of the Git repository.
+        * @param remoteName
+        *            name of the remote, if the remote as configured in
+        *            {@code local}; otherwise null.
+        * @return the transport.
+        * @throws NotSupportedException
+        *             this protocol does not support the URI.
+        * @throws TransportException
+        *             the transport cannot open this URI.
+        */
+       public abstract Transport open(Repository local, URIish uri,
+                       String remoteName)
+                       throws NotSupportedException, TransportException;
+}
index 2a196b51d2ac9d458103a64130e3a274422a79ad..c2e3662ff4411e0e297c532f9f4adc4872cbab72 100644 (file)
@@ -51,20 +51,24 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
 
+import org.eclipse.jgit.JGitText;
+import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Ref.Storage;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.lib.Ref.Storage;
 
 import com.jcraft.jsch.Channel;
 import com.jcraft.jsch.ChannelSftp;
@@ -93,9 +97,34 @@ import com.jcraft.jsch.SftpException;
  * @see WalkFetchConnection
  */
 public class TransportSftp extends SshTransport implements WalkTransport {
-       static boolean canHandle(final URIish uri) {
-               return uri.isRemote() && "sftp".equals(uri.getScheme());
-       }
+       static final TransportProtocol PROTO_SFTP = new TransportProtocol() {
+               public String getName() {
+                       return JGitText.get().transportProtoSFTP;
+               }
+
+               public Set<String> getSchemes() {
+                       return Collections.singleton("sftp"); //$NON-NLS-1$
+               }
+
+               public Set<URIishField> getRequiredFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST,
+                                       URIishField.PATH));
+               }
+
+               public Set<URIishField> getOptionalFields() {
+                       return Collections.unmodifiableSet(EnumSet.of(URIishField.USER,
+                                       URIishField.PASS, URIishField.PORT));
+               }
+
+               public int getDefaultPort() {
+                       return 22;
+               }
+
+               public Transport open(Repository local, URIish uri, String remoteName)
+                               throws NotSupportedException {
+                       return new TransportSftp(local, uri);
+               }
+       };
 
        TransportSftp(final Repository local, final URIish uri) {
                super(local, uri);