TransportSftp: eliminate dependency on Jsch

Introduce an FtpChannel abstraction, which can be obtained from a
RemoteSession. In JSchSession, wrap a JSch ChannelSftp as such an
FtpChannel. The JSch-specific SftpException is also mapped to a
generic FtpException. Rewrite TransportSftp to use only the new
abstraction layer.

This makes it possible to provide alternate ssh/sftp implementations.

Bug: 520927
Change-Id: I379026f7d4122f34931df909a28e73c02cd8a1da
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 1b56ecd..9e8cecb 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -58,4 +58,12 @@
             </message_arguments>
         </filter>
     </resource>
+    <resource path="src/org/eclipse/jgit/transport/RemoteSession.java" type="org.eclipse.jgit.transport.RemoteSession">
+        <filter id="404000815">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.transport.RemoteSession"/>
+                <message_argument value="getFtpChannel()"/>
+            </message_arguments>
+        </filter>
+    </resource>
 </component>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java
new file mode 100644
index 0000000..162a1dc
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FtpChannel.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An interface providing FTP operations over a {@link RemoteSession}. All
+ * operations are supposed to throw {@link FtpException} for remote file system
+ * errors and other IOExceptions on connection errors.
+ *
+ * @since 5.2
+ */
+public interface FtpChannel {
+
+	/**
+	 * An {@link Exception} for reporting SFTP errors.
+	 */
+	static class FtpException extends IOException {
+
+		private static final long serialVersionUID = 7176525179280330876L;
+
+		public static final int OK = 0;
+
+		public static final int EOF = 1;
+
+		public static final int NO_SUCH_FILE = 2;
+
+		public static final int NO_PERMISSION = 3;
+
+		public static final int UNSPECIFIED_FAILURE = 4;
+
+		public static final int PROTOCOL_ERROR = 5;
+
+		public static final int UNSUPPORTED = 8;
+
+		private final int status;
+
+		public FtpException(String message, int status) {
+			super(message);
+			this.status = status;
+		}
+
+		public FtpException(String message, int status, Throwable cause) {
+			super(message, cause);
+			this.status = status;
+		}
+
+		public int getStatus() {
+			return status;
+		}
+	}
+
+	/**
+	 * Connects the {@link FtpChannel} to the remote end.
+	 *
+	 * @param timeout
+	 *            for establishing the FTP connection
+	 * @param unit
+	 *            of the {@code timeout}
+	 * @throws IOException
+	 */
+	void connect(int timeout, TimeUnit unit) throws IOException;
+
+	/**
+	 * Disconnects and {@link FtpChannel}.
+	 */
+	void disconnect();
+
+	/**
+	 * @return whether the {@link FtpChannel} is connected
+	 */
+	boolean isConnected();
+
+	/**
+	 * Changes the current remote directory.
+	 *
+	 * @param path
+	 *            target directory
+	 * @throws IOException
+	 *             if the operation could not be performed remotely
+	 */
+	void cd(String path) throws IOException;
+
+	/**
+	 * @return the current remote directory path
+	 * @throws IOException
+	 */
+	String pwd() throws IOException;
+
+	/**
+	 * Simplified remote directory entry.
+	 */
+	interface DirEntry {
+		String getFilename();
+
+		long getModifiedTime();
+
+		boolean isDirectory();
+	}
+
+	/**
+	 * Lists contents of a remote directory
+	 *
+	 * @param path
+	 *            of the directory to list
+	 * @return the directory entries
+	 * @throws IOException
+	 */
+	Collection<DirEntry> ls(String path) throws IOException;
+
+	/**
+	 * Deletes a directory on the remote file system. The directory must be
+	 * empty.
+	 *
+	 * @param path
+	 *            to delete
+	 * @throws IOException
+	 */
+	void rmdir(String path) throws IOException;
+
+	/**
+	 * Creates a directory on the remote file system.
+	 *
+	 * @param path
+	 *            to create
+	 * @throws IOException
+	 */
+	void mkdir(String path) throws IOException;
+
+	/**
+	 * Obtain an {@link InputStream} to read the contents of a remote file.
+	 *
+	 * @param path
+	 *            of the file to read
+	 *
+	 * @return the stream to read from
+	 * @throws IOException
+	 */
+	InputStream get(String path) throws IOException;
+
+	/**
+	 * Obtain an {@link OutputStream} to write to a remote file. If the file
+	 * exists already, it will be overwritten.
+	 *
+	 * @param path
+	 *            of the file to read
+	 *
+	 * @return the stream to read from
+	 * @throws IOException
+	 */
+	OutputStream put(String path) throws IOException;
+
+	/**
+	 * Deletes a file on the remote file system.
+	 *
+	 * @param path
+	 *            to delete
+	 * @throws IOException
+	 */
+	void rm(String path) throws IOException;
+
+	/**
+	 * Renames a file on the remote file system.
+	 *
+	 * @param from
+	 *            original name of the file
+	 * @param to
+	 *            new name of the file
+	 * @throws IOException
+	 */
+	void rename(String from, String to) throws IOException;
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
index e3ef832..a7797d9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
@@ -52,6 +52,11 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
@@ -59,8 +64,10 @@
 
 import com.jcraft.jsch.Channel;
 import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.ChannelSftp;
 import com.jcraft.jsch.JSchException;
 import com.jcraft.jsch.Session;
+import com.jcraft.jsch.SftpException;
 
 /**
  * Run remote commands using Jsch.
@@ -109,12 +116,24 @@ public void disconnect() {
 	 * @return a channel suitable for Sftp operations.
 	 * @throws com.jcraft.jsch.JSchException
 	 *             on problems getting the channel.
+	 * @deprecated since 5.2; use {@link #getFtpChannel()} instead
 	 */
+	@Deprecated
 	public Channel getSftpChannel() throws JSchException {
 		return sock.openChannel("sftp"); //$NON-NLS-1$
 	}
 
 	/**
+	 * {@inheritDoc}
+	 *
+	 * @since 5.2
+	 */
+	@Override
+	public FtpChannel getFtpChannel() {
+		return new JschFtpChannel();
+	}
+
+	/**
 	 * Implementation of Process for running a single command using Jsch.
 	 * <p>
 	 * Uses the Jsch session to do actual command execution and manage the
@@ -233,4 +252,127 @@ public int waitFor() throws InterruptedException {
 			return exitValue();
 		}
 	}
+
+	private class JschFtpChannel implements FtpChannel {
+
+		private ChannelSftp ftp;
+
+		@Override
+		public void connect(int timeout, TimeUnit unit) throws IOException {
+			try {
+				ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$
+				ftp.connect((int) unit.toMillis(timeout));
+			} catch (JSchException e) {
+				ftp = null;
+				throw new IOException(e.getLocalizedMessage(), e);
+			}
+		}
+
+		@Override
+		public void disconnect() {
+			ftp.disconnect();
+			ftp = null;
+		}
+
+		private <T> T map(Callable<T> op) throws IOException {
+			try {
+				return op.call();
+			} catch (Exception e) {
+				if (e instanceof SftpException) {
+					throw new FtpChannel.FtpException(e.getLocalizedMessage(),
+							((SftpException) e).id, e);
+				}
+				throw new IOException(e.getLocalizedMessage(), e);
+			}
+		}
+
+		@Override
+		public boolean isConnected() {
+			return ftp != null && sock.isConnected();
+		}
+
+		@Override
+		public void cd(String path) throws IOException {
+			map(() -> {
+				ftp.cd(path);
+				return null;
+			});
+		}
+
+		@Override
+		public String pwd() throws IOException {
+			return map(() -> ftp.pwd());
+		}
+
+		@Override
+		public Collection<DirEntry> ls(String path) throws IOException {
+			return map(() -> {
+				List<DirEntry> result = new ArrayList<>();
+				for (Object e : ftp.ls(path)) {
+					ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e;
+					result.add(new DirEntry() {
+
+						@Override
+						public String getFilename() {
+							return entry.getFilename();
+						}
+
+						@Override
+						public long getModifiedTime() {
+							return entry.getAttrs().getMTime();
+						}
+
+						@Override
+						public boolean isDirectory() {
+							return entry.getAttrs().isDir();
+						}
+					});
+				}
+				return result;
+			});
+		}
+
+		@Override
+		public void rmdir(String path) throws IOException {
+			map(() -> {
+				ftp.rm(path);
+				return null;
+			});
+		}
+
+		@Override
+		public void mkdir(String path) throws IOException {
+			map(() -> {
+				ftp.mkdir(path);
+				return null;
+			});
+		}
+
+		@Override
+		public InputStream get(String path) throws IOException {
+			return map(() -> ftp.get(path));
+		}
+
+		@Override
+		public OutputStream put(String path) throws IOException {
+			return map(() -> ftp.put(path));
+		}
+
+		@Override
+		public void rm(String path) throws IOException {
+			map(() -> {
+				ftp.rm(path);
+				return null;
+			});
+		}
+
+		@Override
+		public void rename(String from, String to) throws IOException {
+			map(() -> {
+				ftp.rename(from, to);
+				return null;
+			});
+		}
+
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java
index 525c895..e2109c2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java
@@ -78,10 +78,21 @@ public interface RemoteSession {
 	 *             a TransportException may be thrown (a subclass of
 	 *             java.io.IOException).
 	 */
-	public Process exec(String commandName, int timeout) throws IOException;
+	Process exec(String commandName, int timeout) throws IOException;
+
+	/**
+	 * Obtain an {@link FtpChannel} for performing FTP operations over this
+	 * {@link RemoteSession}. The default implementation returns {@code null}.
+	 *
+	 * @return the {@link FtpChannel}
+	 * @since 5.2
+	 */
+	default FtpChannel getFtpChannel() {
+		return null;
+	}
 
 	/**
 	 * Disconnect the remote session
 	 */
-	public void disconnect();
+	void disconnect();
 }
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 e040e0c..5cf9867 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java
@@ -53,13 +53,14 @@
 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 java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
@@ -73,12 +74,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.SymbolicRef;
 
-import com.jcraft.jsch.Channel;
-import com.jcraft.jsch.ChannelSftp;
-import com.jcraft.jsch.JSchException;
-import com.jcraft.jsch.SftpATTRS;
-import com.jcraft.jsch.SftpException;
-
 /**
  * Transport over the non-Git aware SFTP (SSH based FTP) protocol.
  * <p>
@@ -158,24 +153,16 @@ public PushConnection openPush() throws TransportException {
 		return r;
 	}
 
-	ChannelSftp newSftp() throws TransportException {
-		final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0;
-		try {
-			// @TODO: Fix so that this operation is generic and casting to
-			// JschSession is no longer necessary.
-			final Channel channel = ((JschSession) getSession())
-					.getSftpChannel();
-			channel.connect(tms);
-			return (ChannelSftp) channel;
-		} catch (JSchException je) {
-			throw new TransportException(uri, je.getMessage(), je);
-		}
+	FtpChannel newSftp() throws IOException {
+		FtpChannel channel = getSession().getFtpChannel();
+		channel.connect(getTimeout(), TimeUnit.SECONDS);
+		return channel;
 	}
 
 	class SftpObjectDB extends WalkRemoteObjectDatabase {
 		private final String objectsPath;
 
-		private ChannelSftp ftp;
+		private FtpChannel ftp;
 
 		SftpObjectDB(String path) throws TransportException {
 			if (path.startsWith("/~")) //$NON-NLS-1$
@@ -187,13 +174,13 @@ class SftpObjectDB extends WalkRemoteObjectDatabase {
 				ftp.cd(path);
 				ftp.cd("objects"); //$NON-NLS-1$
 				objectsPath = ftp.pwd();
-			} catch (TransportException err) {
-				close();
-				throw err;
-			} catch (SftpException je) {
+			} catch (FtpChannel.FtpException f) {
 				throw new TransportException(MessageFormat.format(
 						JGitText.get().cannotEnterObjectsPath, path,
-						je.getMessage()), je);
+						f.getMessage()), f);
+			} catch (IOException ioe) {
+				close();
+				throw new TransportException(uri, ioe.getMessage(), ioe);
 			}
 		}
 
@@ -204,13 +191,13 @@ class SftpObjectDB extends WalkRemoteObjectDatabase {
 				ftp.cd(parent.objectsPath);
 				ftp.cd(p);
 				objectsPath = ftp.pwd();
-			} catch (TransportException err) {
-				close();
-				throw err;
-			} catch (SftpException je) {
+			} catch (FtpChannel.FtpException f) {
 				throw new TransportException(MessageFormat.format(
 						JGitText.get().cannotEnterPathFromParent, p,
-						parent.objectsPath, je.getMessage()), je);
+						parent.objectsPath, f.getMessage()), f);
+			} catch (IOException ioe) {
+				close();
+				throw new TransportException(uri, ioe.getMessage(), ioe);
 			}
 		}
 
@@ -238,41 +225,32 @@ WalkRemoteObjectDatabase openAlternate(String location)
 		Collection<String> getPackNames() throws IOException {
 			final List<String> packs = new ArrayList<>();
 			try {
-				@SuppressWarnings("unchecked")
-				final Collection<ChannelSftp.LsEntry> list = ftp.ls("pack"); //$NON-NLS-1$
-				final HashMap<String, ChannelSftp.LsEntry> files;
-				final HashMap<String, Integer> mtimes;
+				Collection<FtpChannel.DirEntry> list = ftp.ls("pack"); //$NON-NLS-1$
+				Set<String> files = list.stream()
+						.map(FtpChannel.DirEntry::getFilename)
+						.collect(Collectors.toSet());
+				HashMap<String, Long> mtimes = new HashMap<>();
 
-				files = new HashMap<>();
-				mtimes = new HashMap<>();
-
-				for (ChannelSftp.LsEntry ent : list)
-					files.put(ent.getFilename(), ent);
-				for (ChannelSftp.LsEntry ent : list) {
-					final String n = ent.getFilename();
-					if (!n.startsWith("pack-") || !n.endsWith(".pack")) //$NON-NLS-1$ //$NON-NLS-2$
+				for (FtpChannel.DirEntry ent : list) {
+					String n = ent.getFilename();
+					if (!n.startsWith("pack-") || !n.endsWith(".pack")) { //$NON-NLS-1$ //$NON-NLS-2$
 						continue;
-
-					final String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$
-					if (!files.containsKey(in))
+					}
+					String in = n.substring(0, n.length() - 5) + ".idx"; //$NON-NLS-1$
+					if (!files.contains(in)) {
 						continue;
-
-					mtimes.put(n, Integer.valueOf(ent.getAttrs().getMTime()));
+					}
+					mtimes.put(n, Long.valueOf(ent.getModifiedTime()));
 					packs.add(n);
 				}
 
-				Collections.sort(packs, new Comparator<String>() {
-					@Override
-					public int compare(String o1, String o2) {
-						return mtimes.get(o2).intValue()
-								- mtimes.get(o1).intValue();
-					}
-				});
-			} catch (SftpException je) {
+				Collections.sort(packs,
+						(o1, o2) -> mtimes.get(o2).compareTo(mtimes.get(o1)));
+			} catch (FtpChannel.FtpException f) {
 				throw new TransportException(
 						MessageFormat.format(JGitText.get().cannotListPackPath,
-								objectsPath, je.getMessage()),
-						je);
+								objectsPath, f.getMessage()),
+						f);
 			}
 			return packs;
 		}
@@ -280,14 +258,14 @@ public int compare(String o1, String o2) {
 		@Override
 		FileStream open(String path) throws IOException {
 			try {
-				final SftpATTRS a = ftp.lstat(path);
-				return new FileStream(ftp.get(path), a.getSize());
-			} catch (SftpException je) {
-				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
+				return new FileStream(ftp.get(path));
+			} catch (FtpChannel.FtpException f) {
+				if (f.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
 					throw new FileNotFoundException(path);
+				}
 				throw new TransportException(MessageFormat.format(
 						JGitText.get().cannotGetObjectsPath, objectsPath, path,
-						je.getMessage()), je);
+						f.getMessage()), f);
 			}
 		}
 
@@ -295,12 +273,15 @@ FileStream open(String path) throws IOException {
 		void deleteFile(String path) throws IOException {
 			try {
 				ftp.rm(path);
-			} catch (SftpException je) {
-				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
+			} catch (FileNotFoundException e) {
+				return;
+			} catch (FtpChannel.FtpException f) {
+				if (f.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
 					return;
+				}
 				throw new TransportException(MessageFormat.format(
 						JGitText.get().cannotDeleteObjectsPath, objectsPath,
-						path, je.getMessage()), je);
+						path, f.getMessage()), f);
 			}
 
 			// Prune any now empty directories.
@@ -312,7 +293,7 @@ void deleteFile(String path) throws IOException {
 					dir = dir.substring(0, s);
 					ftp.rmdir(dir);
 					s = dir.lastIndexOf('/');
-				} catch (SftpException je) {
+				} catch (IOException je) {
 					// If we cannot delete it, leave it alone. It may have
 					// entries still in it, or maybe we lack write access on
 					// the parent. Either way it isn't a fatal error.
@@ -325,22 +306,29 @@ void deleteFile(String path) throws IOException {
 		@Override
 		OutputStream writeFile(String path, ProgressMonitor monitor,
 				String monitorTask) throws IOException {
+			Throwable err = null;
 			try {
 				return ftp.put(path);
-			} catch (SftpException je) {
-				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
+			} catch (FileNotFoundException e) {
+				mkdir_p(path);
+			} catch (FtpChannel.FtpException je) {
+				if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
 					mkdir_p(path);
-					try {
-						return ftp.put(path);
-					} catch (SftpException je2) {
-						je = je2;
-					}
+				} else {
+					err = je;
 				}
-
-				throw new TransportException(MessageFormat.format(
-						JGitText.get().cannotWriteObjectsPath, objectsPath,
-						path, je.getMessage()), je);
 			}
+			if (err == null) {
+				try {
+					return ftp.put(path);
+				} catch (IOException e) {
+					err = e;
+				}
+			}
+			throw new TransportException(
+					MessageFormat.format(JGitText.get().cannotWriteObjectsPath,
+							objectsPath, path, err.getMessage()),
+					err);
 		}
 
 		@Override
@@ -350,15 +338,15 @@ void writeFile(String path, byte[] data) throws IOException {
 				super.writeFile(lock, data);
 				try {
 					ftp.rename(lock, path);
-				} catch (SftpException je) {
+				} catch (IOException e) {
 					throw new TransportException(MessageFormat.format(
 							JGitText.get().cannotWriteObjectsPath, objectsPath,
-							path, je.getMessage()), je);
+							path, e.getMessage()), e);
 				}
 			} catch (IOException err) {
 				try {
 					ftp.rm(lock);
-				} catch (SftpException e) {
+				} catch (IOException e) {
 					// Ignore deletion failure, we are already
 					// failing anyway.
 				}
@@ -372,23 +360,30 @@ private void mkdir_p(String path) throws IOException {
 				return;
 
 			path = path.substring(0, s);
+			Throwable err = null;
 			try {
 				ftp.mkdir(path);
-			} catch (SftpException je) {
-				if (je.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
+				return;
+			} catch (FileNotFoundException f) {
+				mkdir_p(path);
+			} catch (FtpChannel.FtpException je) {
+				if (je.getStatus() == FtpChannel.FtpException.NO_SUCH_FILE) {
 					mkdir_p(path);
-					try {
-						ftp.mkdir(path);
-						return;
-					} catch (SftpException je2) {
-						je = je2;
-					}
+				} else {
+					err = je;
 				}
-
-				throw new TransportException(MessageFormat.format(
-						JGitText.get().cannotMkdirObjectPath, objectsPath, path,
-						je.getMessage()), je);
 			}
+			if (err == null) {
+				try {
+					ftp.mkdir(path);
+					return;
+				} catch (IOException e) {
+					err = e;
+				}
+			}
+			throw new TransportException(MessageFormat.format(
+						JGitText.get().cannotMkdirObjectPath, objectsPath, path,
+					err.getMessage()), err);
 		}
 
 		Map<String, Ref> readAdvertisedRefs() throws TransportException {
@@ -399,28 +394,28 @@ Map<String, Ref> readAdvertisedRefs() throws TransportException {
 			return avail;
 		}
 
-		@SuppressWarnings("unchecked")
 		private void readLooseRefs(TreeMap<String, Ref> avail, String dir,
 				String prefix) throws TransportException {
-			final Collection<ChannelSftp.LsEntry> list;
+			final Collection<FtpChannel.DirEntry> list;
 			try {
 				list = ftp.ls(dir);
-			} catch (SftpException je) {
+			} catch (IOException e) {
 				throw new TransportException(MessageFormat.format(
 						JGitText.get().cannotListObjectsPath, objectsPath, dir,
-						je.getMessage()), je);
+						e.getMessage()), e);
 			}
 
-			for (ChannelSftp.LsEntry ent : list) {
-				final String n = ent.getFilename();
+			for (FtpChannel.DirEntry ent : list) {
+				String n = ent.getFilename();
 				if (".".equals(n) || "..".equals(n)) //$NON-NLS-1$ //$NON-NLS-2$
 					continue;
 
-				final String nPath = dir + "/" + n; //$NON-NLS-1$
-				if (ent.getAttrs().isDir())
+				String nPath = dir + "/" + n; //$NON-NLS-1$
+				if (ent.isDirectory()) {
 					readLooseRefs(avail, nPath, prefix + n + "/"); //$NON-NLS-1$
-				else
+				} else {
 					readRef(avail, nPath, prefix + n);
+				}
 			}
 		}
 
@@ -437,10 +432,10 @@ private Ref readRef(TreeMap<String, Ref> avail, String path,
 						err.getMessage()), err);
 			}
 
-			if (line == null)
+			if (line == null) {
 				throw new TransportException(
 						MessageFormat.format(JGitText.get().emptyRef, name));
-
+			}
 			if (line.startsWith("ref: ")) { //$NON-NLS-1$
 				final String target = line.substring("ref: ".length()); //$NON-NLS-1$
 				Ref r = avail.get(target);
@@ -465,8 +460,9 @@ private Ref readRef(TreeMap<String, Ref> avail, String path,
 		}
 
 		private Storage loose(Ref r) {
-			if (r != null && r.getStorage() == Storage.PACKED)
+			if (r != null && r.getStorage() == Storage.PACKED) {
 				return Storage.LOOSE_PACKED;
+			}
 			return Storage.LOOSE;
 		}
 
@@ -474,8 +470,9 @@ private Storage loose(Ref r) {
 		void close() {
 			if (ftp != null) {
 				try {
-					if (ftp.isConnected())
+					if (ftp.isConnected()) {
 						ftp.disconnect();
+					}
 				} finally {
 					ftp = null;
 				}