From 305a8ac45f350580957743b3b3aa4c4eca0a6396 Mon Sep 17 00:00:00 2001 From: "Shawn O. Pearce" Date: Mon, 7 Mar 2011 15:01:49 -0800 Subject: [PATCH] Make the supported Transports extensible and discoverable 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 --- .../transport/ReceivePackRefFilterTest.java | 4 +- .../eclipse/jgit/transport/URIishTest.java | 2 +- .../org/eclipse/jgit/JGitText.properties | 8 + .../src/org/eclipse/jgit/JGitText.java | 8 + .../org/eclipse/jgit/api/FetchCommand.java | 33 +-- .../api/errors/InvalidRemoteException.java | 10 +- .../org/eclipse/jgit/transport/Transport.java | 200 +++++++++++----- .../jgit/transport/TransportAmazonS3.java | 33 ++- .../jgit/transport/TransportBundleFile.java | 61 +++-- .../jgit/transport/TransportGitAnon.java | 34 ++- .../jgit/transport/TransportGitSsh.java | 66 ++++-- .../eclipse/jgit/transport/TransportHttp.java | 73 +++++- .../jgit/transport/TransportLocal.java | 59 +++-- .../jgit/transport/TransportProtocol.java | 221 ++++++++++++++++++ .../eclipse/jgit/transport/TransportSftp.java | 37 ++- 15 files changed, 703 insertions(+), 146 deletions(-) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackRefFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackRefFilterTest.java index 5fd76d4ad..c58252a23 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackRefFilterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackRefFilterTest.java @@ -143,7 +143,7 @@ public class ReceivePackRefFilterTest extends LocalDiskRepositoryTestCase { @Test public void testFilterHidesPrivate() throws Exception { Map 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(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java index 34dadf39e..bdc9b3ecd 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java @@ -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)); } } diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties index f96d4261c..9fe17fef0 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties @@ -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} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java index 9ef2cd93e..e07a4252e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java @@ -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; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java index 9594dfebe..51dc5295f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java @@ -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 { 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)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRemoteException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRemoteException.java index 4104fd669..3f059b79c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRemoteException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/InvalidRemoteException.java @@ -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); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java index 497fb844b..073b1ac1a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java @@ -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> protocols = + new CopyOnWriteArrayList>(); + + 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. + *

+ * 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. + *

+ * 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(proto)); + } + + /** + * Unregister a TransportProtocol instance. + *

+ * 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 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 getTransportProtocols() { + int cnt = protocols.size(); + List res = new ArrayList(cnt); + for (WeakReference 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. *

@@ -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 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 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 transports = new ArrayList(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 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 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 openAll(final Repository local, final RemoteConfig cfg, final Operation op) - throws NotSupportedException { + throws NotSupportedException, TransportException { final List uris = getURIs(cfg, op); final List transports = new ArrayList(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 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)); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java index 79b88b6a7..0f65e0cac 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java @@ -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 getSchemes() { + return Collections.singleton(S3_SCHEME); + } + + public Set getRequiredFields() { + return Collections.unmodifiableSet(EnumSet.of(URIishField.USER, + URIishField.HOST, URIishField.PATH)); + } + + public Set 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; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java index c47833f21..15aa1fff0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportBundleFile.java @@ -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 schemeSet = Collections + .unmodifiableSet(new LinkedHashSet(Arrays + .asList(schemeNames))); + + @Override + public String getName() { + return JGitText.get().transportProtoBundleFile; } - return false; - } + public Set 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 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java index 5ad57768f..081e7a90b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java @@ -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 getSchemes() { + return Collections.singleton("git"); //$NON-NLS-1$ + } + + public Set getRequiredFields() { + return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST, + URIishField.PATH)); + } + + public Set 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); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java index 7ad5fc71c..a0cb8a380 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java @@ -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 schemeSet = Collections + .unmodifiableSet(new LinkedHashSet(Arrays + .asList(schemeNames))); + + public String getName() { + return JGitText.get().transportProtoSSH; + } + + public Set getSchemes() { + return schemeSet; + } + + public Set getRequiredFields() { + return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST, + URIishField.PATH)); + } + + public Set 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); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index 5c4e11036..bb69b8c07 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -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 schemeSet = Collections + .unmodifiableSet(new LinkedHashSet(Arrays + .asList(schemeNames))); + + public String getName() { + return JGitText.get().transportProtoHTTP; + } + + public Set getSchemes() { + return schemeSet; + } + + public Set getRequiredFields() { + return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST, + URIishField.PATH)); + } + + public Set 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 getSchemes() { + return Collections.singleton("ftp"); //$NON-NLS-1$ + } + + public Set getRequiredFields() { + return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST, + URIishField.PATH)); + } + + public Set 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; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java index 3ede748b3..280b7c58e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java @@ -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 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 index 000000000..4652e01de --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportProtocol.java @@ -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. + *

+ * Implementations of this class are typically immutable singletons held by + * static class members, for example: + * + *

+ * class MyTransport extends Transport {
+ * 	static final TransportProtocol PROTO = new TransportProtocol() {
+ * 		public String getName() {
+ * 			return "My Protocol";
+ * 		}
+ * 	};
+ * }
+ * 
+ * + * 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 getSchemes() { + return Collections.emptySet(); + } + + /** @return immutable set of URIishFields that must be filled in. */ + public Set getRequiredFields() { + return Collections.unmodifiableSet(EnumSet.of(URIishField.PATH)); + } + + /** @return immutable set of URIishFields that may be filled in. */ + public Set 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. + *

+ * 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. + *

+ * 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 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. + *

+ * 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. + *

+ * 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; +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java index 2a196b51d..c2e3662ff 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java @@ -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 getSchemes() { + return Collections.singleton("sftp"); //$NON-NLS-1$ + } + + public Set getRequiredFields() { + return Collections.unmodifiableSet(EnumSet.of(URIishField.HOST, + URIishField.PATH)); + } + + public Set 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);