You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/02/18 18:20:10 UTC

[1/2] mina-sshd git commit: [SSHD-798] Provide pluggable GitLocationResolver for the GIT commands

Repository: mina-sshd
Updated Branches:
  refs/heads/master 59a7fcf88 -> f8a3e7299


[SSHD-798] Provide pluggable GitLocationResolver for the GIT commands


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/f8a3e729
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/f8a3e729
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/f8a3e729

Branch: refs/heads/master
Commit: f8a3e72993e3536e4b277e39c8ea864947c93098
Parents: e0d6350
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Sun Feb 18 17:06:35 2018 +0200
Committer: Lyor Goldstein <ly...@gmail.com>
Committed: Sun Feb 18 20:22:50 2018 +0200

----------------------------------------------------------------------
 README.md                                       | 37 +++++++++--
 .../org/apache/sshd/git/AbstractGitCommand.java | 67 ++++++++++++++++++--
 .../sshd/git/AbstractGitCommandFactory.java     | 53 ++++++++++------
 .../apache/sshd/git/GitLocationResolver.java    | 55 ++++++++++++++++
 .../sshd/git/GitLocationResolverCarrier.java    | 29 +++++++++
 .../apache/sshd/git/pack/GitPackCommand.java    | 37 ++++++++---
 .../sshd/git/pack/GitPackCommandFactory.java    | 22 +++++--
 .../sshd/git/pgm/EmbeddedCommandRunner.java     | 29 +++------
 .../org/apache/sshd/git/pgm/GitPgmCommand.java  | 39 +++++++-----
 .../sshd/git/pgm/GitPgmCommandFactory.java      | 22 +++++--
 .../sshd/git/pack/GitPackCommandTest.java       |  8 +--
 .../apache/sshd/git/pgm/GitPgmCommandTest.java  | 10 +--
 12 files changed, 306 insertions(+), 102 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 283cc85..a4925a3 100644
--- a/README.md
+++ b/README.md
@@ -1364,20 +1364,41 @@ client instance and re-use it:
 
 ### Server-side
 
-See `GitPackCommandFactory` and `GitPgmCommandFactory`. These command factories accept a delegate to which non-_git_ commands are routed:
+See `GitPackCommandFactory` and `GitPgmCommandFactory` - in order for the various commands to function correctly, they require a `GitLocationResolver`
+that is invoked in order to allow the user to decide which is the correct GIT repository root location for a given command. The resolver is provided
+with all the relevant details - including the command and server session through which the command was received:
 
 
 ```java
 
-    sshd.setCommandFactory(new GitPackCommandFactory(rootDir, new MyCommandFactory()));
+    GitLocationResolver resolver = (cmd, session, fs) -> ...consult some code - perhaps based on the authenticated username...
+    sshd.setCommandFactory(new GitPackCommandFactory().withGitLocationResolver(resolver));
+
+```
+
+ These command factories also accept a delegate to which non-_git_ commands are routed:
+
+
+```java
+
+    sshd.setCommandFactory(new GitPackCommandFactory()
+        .withDelegate(new MyCommandFactory())
+        .withGitLocationResolver(resolver));
 
     // Here is how it looks if SCP is also requested
-    sshd.setCommandFactory(new GitPackCommandFactory(rootDir, new ScpCommandFactory(new MyCommandFactory())))
-    // or
-    sshd.setCommandFactory(new ScpCommandFactory(new GitPackCommandFactory(rootDir, new MyCommandFactory())))
+    sshd.setCommandFactory(new GitPackCommandFactory()
+        .withDelegate(new ScpCommandFactory()
+            .withDelegate(new MyCommandFactory()))
+        .withGitLocationResolver(resolver));
+        
     // or
-    sshd.setCommandFactory(new GitPackCommandFactory(rootDir, new ScpCommandFactory(new MyCommandFactory())))
+    sshd.setCommandFactory(new ScpCommandFactory()
+        .withDelegate(new GitPackCommandFactory()
+            .withDelegate(new MyCommandFactory())
+            .withGitLocationResolver(resolver)));
+            
     // or any other combination ...
+
 ```
 
 as with all other built-in commands, the factories allow the user to provide an `ExecutorService` in order to control the spwaned threads
@@ -1387,12 +1408,14 @@ is completed (regardless of whether successful or not):
 
 ```java
 
-    sshd.setCommandFactory(new GitPackCommandFactory(rootDir, new MyCommandFactory())
+    sshd.setCommandFactory(new GitPackCommandFactory(resolver)
+        .withDelegate(new MyCommandFactory())
         .withExecutorService(myService)
         .withShutdownOnExit(false));
 
 ```
 
+
 ## LDAP adaptors
 
 The _sshd-ldap_ artifact contains an [LdapPasswordAuthenticator](https://issues.apache.org/jira/browse/SSHD-607) and an [LdapPublicKeyAuthenticator](https://issues.apache.org/jira/browse/SSHD-608) that have been written along the same lines as the [openssh-ldap-publickey](https://github.com/AndriiGrytsenko/openssh-ldap-publickey) project. The authenticators can be easily configured to match most LDAP schemes, or alternatively serve as base classes for code that extends them and adds proprietary logic.

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
index 2ed85b0..d4dacfa 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
@@ -19,34 +19,65 @@
 
 package org.apache.sshd.git;
 
+import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.file.FileSystem;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.ExecutorService;
 
 import org.apache.sshd.common.channel.ChannelOutputStream;
+import org.apache.sshd.common.file.FileSystemAware;
 import org.apache.sshd.server.AbstractCommandSupport;
+import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionHolder;
 
 /**
  * Provides basic support for GIT command implementations
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public abstract class AbstractGitCommand extends AbstractCommandSupport {
+public abstract class AbstractGitCommand
+        extends AbstractCommandSupport
+        implements SessionAware, FileSystemAware, ServerSessionHolder, GitLocationResolverCarrier {
     public static final int CHAR = 0x001;
     public static final int DELIMITER = 0x002;
     public static final int STARTQUOTE = 0x004;
     public static final int ENDQUOTE = 0x008;
 
-    private String rootDir;
+    private final GitLocationResolver rootDirResolver;
+    private FileSystem fileSystem;
+    private ServerSession session;
 
-    protected AbstractGitCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
+    protected AbstractGitCommand(GitLocationResolver rootDirResolver, String command, ExecutorService executorService, boolean shutdownOnExit) {
         super(command, executorService, shutdownOnExit);
-        this.rootDir = rootDir;
+        this.rootDirResolver = Objects.requireNonNull(rootDirResolver, "No GIT root directory resolver provided");
     }
 
-    public String getRootDir() {
-        return rootDir;
+    @Override
+    public GitLocationResolver getGitLocationResolver() {
+        return rootDirResolver;
+    }
+
+    public FileSystem getFileSystem() {
+        return fileSystem;
+    }
+
+    @Override
+    public void setFileSystem(FileSystem fileSystem) {
+        this.fileSystem = fileSystem;
+    }
+
+    @Override
+    public ServerSession getServerSession() {
+        return session;
+    }
+
+    @Override
+    public void setSession(ServerSession session) {
+        this.session = session;
     }
 
     @Override
@@ -65,6 +96,30 @@ public abstract class AbstractGitCommand extends AbstractCommandSupport {
         }
     }
 
+    @Override
+    public void destroy() {
+        try {
+            super.destroy();
+        } finally {
+            FileSystem fs = getFileSystem();
+            if (fs != null) {
+                try {
+                    fs.close();
+                } catch (UnsupportedOperationException | IOException e) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("destroy({}) - failed ({}) to close file system={}: {}",
+                                this, e.getClass().getSimpleName(), fs, e.getMessage());
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + "[session=" + getServerSession() + "]";
+    }
+
     /**
      * Parses delimited string and returns an array containing the tokens. This
      * parser obeys quotes, so the delimiter character will be ignored if it is

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
index 337643d..0d42626 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
@@ -32,34 +32,38 @@ import org.apache.sshd.server.scp.UnknownCommand;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public abstract class AbstractGitCommandFactory implements CommandFactory, ExecutorServiceCarrier {
-    private final String rootDir;
-    private final CommandFactory delegate;
+public abstract class AbstractGitCommandFactory implements CommandFactory, ExecutorServiceCarrier, GitLocationResolverCarrier {
     private final String cmdPrefix;
+    private GitLocationResolver rootDirResolver;
+    private CommandFactory delegate;
     private ExecutorService executorService;
     private boolean shutdownOnExit;
 
-    protected AbstractGitCommandFactory(String rootDir, CommandFactory delegate, String cmdPrefix) {
-        this.rootDir = rootDir;
-        this.delegate = delegate;
+    /**
+     * @param cmdPrefix The command prefix used to detect and intercept GIT commands handled by this
+     * factory (never {@code null}/empty)
+     */
+    protected AbstractGitCommandFactory(String cmdPrefix) {
         this.cmdPrefix = ValidateUtils.checkNotNullAndNotEmpty(cmdPrefix, "No command prefix provided");
     }
 
-    public AbstractGitCommandFactory withExecutorService(ExecutorService executorService) {
-        this.executorService = executorService;
-        return this;
+    public String getCommandPrefix() {
+        return cmdPrefix;
     }
 
-    public String getRootDir() {
-        return rootDir;
+    @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
     }
 
-    public CommandFactory getDelegate() {
-        return delegate;
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
     }
 
-    public String getCommandPrefix() {
-        return cmdPrefix;
+    public AbstractGitCommandFactory withExecutorService(ExecutorService executorService) {
+        this.executorService = executorService;
+        return this;
     }
 
     public AbstractGitCommandFactory withShutdownOnExit(boolean shutdownOnExit) {
@@ -68,13 +72,22 @@ public abstract class AbstractGitCommandFactory implements CommandFactory, Execu
     }
 
     @Override
-    public ExecutorService getExecutorService() {
-        return executorService;
+    public GitLocationResolver getGitLocationResolver() {
+        return rootDirResolver;
     }
 
-    @Override
-    public boolean isShutdownOnExit() {
-        return shutdownOnExit;
+    public AbstractGitCommandFactory withGitLocationResolver(GitLocationResolver rootDirResolver) {
+        this.rootDirResolver = rootDirResolver;
+        return this;
+    }
+
+    public CommandFactory getDelegate() {
+        return delegate;
+    }
+
+    public AbstractGitCommandFactory withDelegate(CommandFactory delegate) {
+        this.delegate = delegate;
+        return this;
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/GitLocationResolver.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/GitLocationResolver.java b/sshd-git/src/main/java/org/apache/sshd/git/GitLocationResolver.java
new file mode 100644
index 0000000..7679c90
--- /dev/null
+++ b/sshd-git/src/main/java/org/apache/sshd/git/GitLocationResolver.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.git;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.util.Objects;
+
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * Used by the GIT command(s) to resolve the root directory of the GIT repository
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface GitLocationResolver {
+    /**
+     * @param command The received command
+     * @param session The {@link ServerSession} through which the command was received
+     * @param fs The {@link FileSystem} associated with the server session
+     * @return The local GIT repository root path
+     * @throws IOException If failed to resolve
+     */
+    Path resolveRootDirectory(String command, ServerSession session, FileSystem fs) throws IOException;
+
+    /**
+     * Creates a resolver that returns the same root directory for any invocation of
+     * {@link #resolveRootDirectory(String, ServerSession, FileSystem) resolveRootDirectory}
+     *
+     * @param rootDir The (never {@code null}) root directory to return
+     * @return The wrapper resolver
+     */
+    static GitLocationResolver constantPath(Path rootDir) {
+        Objects.requireNonNull(rootDir, "No root directory provided");
+        return (cmd, session, fs) -> rootDir;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/GitLocationResolverCarrier.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/GitLocationResolverCarrier.java b/sshd-git/src/main/java/org/apache/sshd/git/GitLocationResolverCarrier.java
new file mode 100644
index 0000000..2b53524
--- /dev/null
+++ b/sshd-git/src/main/java/org/apache/sshd/git/GitLocationResolverCarrier.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.git;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface GitLocationResolverCarrier {
+    GitLocationResolver getGitLocationResolver();
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
index 8aebac5..bb123da 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
@@ -18,11 +18,15 @@
  */
 package org.apache.sshd.git.pack;
 
-import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.git.AbstractGitCommand;
+import org.apache.sshd.git.GitLocationResolver;
 import org.apache.sshd.server.Environment;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
@@ -38,15 +42,15 @@ import org.eclipse.jgit.util.FS;
  */
 public class GitPackCommand extends AbstractGitCommand {
     /**
-     * @param rootDir Root directory for the command
+     * @param rootDirResolver Resolver for GIT root directory
      * @param command Command to execute
      * @param executorService An {@link ExecutorService} to be used when {@link #start(Environment)}-ing
      * execution. If {@code null} an ad-hoc single-threaded service is created and used.
      * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()} will be called when
      * command terminates - unless it is the ad-hoc service, which will be shutdown regardless
      */
-    public GitPackCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
-        super(rootDir, command, executorService, shutdownOnExit);
+    public GitPackCommand(GitLocationResolver rootDirResolver, String command, ExecutorService executorService, boolean shutdownOnExit) {
+        super(rootDirResolver, command, executorService, shutdownOnExit);
     }
 
     @Override
@@ -71,13 +75,13 @@ public class GitPackCommand extends AbstractGitCommand {
                 throw new IllegalArgumentException("Invalid git command line: " + command);
             }
 
-            String rootDir = getRootDir();
-            File srcGitdir = new File(rootDir, args[1]);
-            RepositoryCache.FileKey key = RepositoryCache.FileKey.lenient(srcGitdir, FS.DETECTED);
+            String subCommand = args[0];
+            Path rootDir = resolveRootDirectory(command, subCommand, args[1]);
+            RepositoryCache.FileKey key = RepositoryCache.FileKey.lenient(rootDir.toFile(), FS.DETECTED);
             Repository db = key.open(true /* must exist */);
-            if (RemoteConfig.DEFAULT_UPLOAD_PACK.equals(args[0])) {
+            if (RemoteConfig.DEFAULT_UPLOAD_PACK.equals(subCommand)) {
                 new UploadPack(db).upload(getInputStream(), getOutputStream(), getErrorStream());
-            } else if (RemoteConfig.DEFAULT_RECEIVE_PACK.equals(args[0])) {
+            } else if (RemoteConfig.DEFAULT_RECEIVE_PACK.equals(subCommand)) {
                 new ReceivePack(db).receive(getInputStream(), getOutputStream(), getErrorStream());
             } else {
                 throw new IllegalArgumentException("Unknown git command: " + command);
@@ -88,4 +92,19 @@ public class GitPackCommand extends AbstractGitCommand {
             onExit(-1, t.getClass().getSimpleName());
         }
     }
+
+    protected Path resolveRootDirectory(String command, String subCommand, String pathArg) throws IOException {
+        GitLocationResolver resolver = getGitLocationResolver();
+        Path rootDir = resolver.resolveRootDirectory(command, getServerSession(), getFileSystem());
+        ValidateUtils.checkState(rootDir != null, "No root directory provided for %s command", command);
+        int len = GenericUtils.length(pathArg);
+        // Strip any leading path separator since we use relative to root
+        if ((len > 0) && (pathArg.charAt(0) == '/')) {
+            pathArg = (len > 1) ? pathArg.substring(1) : "";
+            len--;
+        }
+
+        ValidateUtils.checkNotNullAndNotEmpty(pathArg, "No %s command sub-path specified", subCommand);
+        return rootDir.resolve(pathArg);
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
index e12c228..a21365f 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
@@ -21,6 +21,7 @@ package org.apache.sshd.git.pack;
 import java.util.concurrent.ExecutorService;
 
 import org.apache.sshd.git.AbstractGitCommandFactory;
+import org.apache.sshd.git.GitLocationResolver;
 import org.apache.sshd.server.CommandFactory;
 
 /**
@@ -31,12 +32,23 @@ import org.apache.sshd.server.CommandFactory;
 public class GitPackCommandFactory extends AbstractGitCommandFactory {
     public static final String GIT_COMMAND_PREFIX = "git-";
 
-    public GitPackCommandFactory(String rootDir) {
-        this(rootDir,  null);
+    public GitPackCommandFactory() {
+        super(GIT_COMMAND_PREFIX);
     }
 
-    public GitPackCommandFactory(String rootDir, CommandFactory delegate) {
-        super(rootDir, delegate, GIT_COMMAND_PREFIX);
+    public GitPackCommandFactory(GitLocationResolver resolver) {
+        super(GIT_COMMAND_PREFIX);
+        withGitLocationResolver(resolver);
+    }
+
+    @Override
+    public GitPackCommandFactory withDelegate(CommandFactory delegate) {
+        return (GitPackCommandFactory) super.withDelegate(delegate);
+    }
+
+    @Override
+    public GitPackCommandFactory withGitLocationResolver(GitLocationResolver rootDirResolver) {
+        return (GitPackCommandFactory) super.withGitLocationResolver(rootDirResolver);
     }
 
     @Override
@@ -51,6 +63,6 @@ public class GitPackCommandFactory extends AbstractGitCommandFactory {
 
     @Override
     public GitPackCommand createGitCommand(String command) {
-        return new GitPackCommand(getRootDir(), command, getExecutorService(), isShutdownOnExit());
+        return new GitPackCommand(getGitLocationResolver(), command, getExecutorService(), isShutdownOnExit());
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
index 204dc80..20c0902 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
@@ -26,9 +26,11 @@ import java.io.PrintWriter;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.nio.file.Path;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.Repository;
@@ -66,10 +68,10 @@ public class EmbeddedCommandRunner {
     @Argument(index = 1, metaVar = "metaVar_arg")
     private List<String> arguments = new ArrayList<>();
 
-    private String rootDir;
+    private Path rootDir;
 
-    public EmbeddedCommandRunner(String rootDir) {
-        this.rootDir = rootDir;
+    public EmbeddedCommandRunner(Path rootDir) {
+        this.rootDir = Objects.requireNonNull(rootDir, "No root directory specified");
     }
 
     /**
@@ -126,24 +128,9 @@ public class EmbeddedCommandRunner {
             throw new Die(true);
         }
 
-        gitdir = new File(rootDir, gitdir).getPath();
-
-        final TextBuiltin cmd = subcommand;
-//        cmd.ins = in;
-//        cmd.outs = out;
-//        cmd.errs = err;
-//        if (cmd.requiresRepository())
-//            cmd.init(openGitDir(gitdir), null);
-//        else
-//            cmd.init(null, gitdir);
-//        try {
-//            cmd.execute(arguments.toArray(new String[arguments.size()]));
-//        } finally {
-//            if (cmd.outw != null)
-//                cmd.outw.flush();
-//            if (cmd.errw != null)
-//                cmd.errw.flush();
-//        }
+        gitdir = Objects.toString(rootDir.resolve(gitdir));
+
+        TextBuiltin cmd = subcommand;
         set(cmd, "ins", in);
         set(cmd, "outs", out);
         set(cmd, "errs", err);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
index 2b7a1ab..a80012d 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
@@ -21,12 +21,14 @@ package org.apache.sshd.git.pgm;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.git.AbstractGitCommand;
+import org.apache.sshd.git.GitLocationResolver;
 import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
 
 /**
  * TODO Add javadoc
@@ -35,49 +37,52 @@ import org.apache.sshd.server.ExitCallback;
  */
 public class GitPgmCommand extends AbstractGitCommand {
     /**
-     * @param rootDir Root directory for the command
+     * @param rootDirResolver Resolver for GIT root directory
      * @param command Command to execute
      * @param executorService An {@link ExecutorService} to be used when {@link #start(Environment)}-ing
      * execution. If {@code null} an ad-hoc single-threaded service is created and used.
      * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()} will be called when
      * command terminates - unless it is the ad-hoc service, which will be shutdown regardless
      */
-    public GitPgmCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
-        super(rootDir, command, executorService, shutdownOnExit);
+    public GitPgmCommand(GitLocationResolver rootDirResolver, String command, ExecutorService executorService, boolean shutdownOnExit) {
+        super(rootDirResolver, command, executorService, shutdownOnExit);
     }
 
     @Override
     public void run() {
         String command = getCommand();
-        ExitCallback callback = getExitCallback();
         OutputStream err = getErrorStream();
         try {
             List<String> strs = parseDelimitedString(command, " ", true);
             String[] args = strs.toArray(new String[strs.size()]);
             for (int i = 0; i < args.length; i++) {
-                if (args[i].startsWith("'") && args[i].endsWith("'")) {
-                    args[i] = args[i].substring(1, args[i].length() - 1);
+                String argVal = args[i];
+                if (argVal.startsWith("'") && argVal.endsWith("'")) {
+                    args[i] = argVal.substring(1, argVal.length() - 1);
+                    argVal = args[i];
                 }
-                if (args[i].startsWith("\"") && args[i].endsWith("\"")) {
-                    args[i] = args[i].substring(1, args[i].length() - 1);
+
+                if (argVal.startsWith("\"") && argVal.endsWith("\"")) {
+                    args[i] = argVal.substring(1, argVal.length() - 1);
+                    argVal = args[i];
                 }
             }
 
-            new EmbeddedCommandRunner(getRootDir()).execute(args, getInputStream(), getOutputStream(), err);
-            if (callback != null) {
-                callback.onExit(0);
-            }
+            GitLocationResolver resolver = getGitLocationResolver();
+            Path rootDir = resolver.resolveRootDirectory(command, getServerSession(), getFileSystem());
+            ValidateUtils.checkState(rootDir != null, "No root directory provided for %s command", command);
+
+            new EmbeddedCommandRunner(rootDir).execute(args, getInputStream(), getOutputStream(), err);
+            onExit(0);
         } catch (Throwable t) {
             try {
                 err.write((t.getMessage() + "\n").getBytes(StandardCharsets.UTF_8));
                 err.flush();
             } catch (IOException e) {
                 log.warn("Failed {} to flush command={} failure: {}",
-                        e.getClass().getSimpleName(), command, e.getMessage());
-            }
-            if (callback != null) {
-                callback.onExit(-1);
+                    e.getClass().getSimpleName(), command, e.getMessage());
             }
+            onExit(-1, t.getMessage());
         }
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
index cba6c4e..7e28fe8 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
@@ -21,6 +21,7 @@ package org.apache.sshd.git.pgm;
 import java.util.concurrent.ExecutorService;
 
 import org.apache.sshd.git.AbstractGitCommandFactory;
+import org.apache.sshd.git.GitLocationResolver;
 import org.apache.sshd.server.CommandFactory;
 
 /**
@@ -31,12 +32,23 @@ import org.apache.sshd.server.CommandFactory;
 public class GitPgmCommandFactory extends AbstractGitCommandFactory {
     public static final String GIT_COMMAND_PREFIX = "git ";
 
-    public GitPgmCommandFactory(String rootDir) {
-        this(rootDir,  null);
+    public GitPgmCommandFactory() {
+        super(GIT_COMMAND_PREFIX);
     }
 
-    public GitPgmCommandFactory(String rootDir, CommandFactory delegate) {
-        super(rootDir, delegate, GIT_COMMAND_PREFIX);
+    public GitPgmCommandFactory(GitLocationResolver resolver) {
+        super(GIT_COMMAND_PREFIX);
+        withGitLocationResolver(resolver);
+    }
+
+    @Override
+    public GitPgmCommandFactory withDelegate(CommandFactory delegate) {
+        return (GitPgmCommandFactory) super.withDelegate(delegate);
+    }
+
+    @Override
+    public GitPgmCommandFactory withGitLocationResolver(GitLocationResolver rootDirResolver) {
+        return (GitPgmCommandFactory) super.withGitLocationResolver(rootDirResolver);
     }
 
     @Override
@@ -51,6 +63,6 @@ public class GitPgmCommandFactory extends AbstractGitCommandFactory {
 
     @Override
     public GitPgmCommand createGitCommand(String command) {
-        return new GitPgmCommand(getRootDir(), command.substring(GIT_COMMAND_PREFIX.length()), getExecutorService(), isShutdownOnExit());
+        return new GitPgmCommand(getGitLocationResolver(), command.substring(GIT_COMMAND_PREFIX.length()), getExecutorService(), isShutdownOnExit());
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/test/java/org/apache/sshd/git/pack/GitPackCommandTest.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/test/java/org/apache/sshd/git/pack/GitPackCommandTest.java b/sshd-git/src/test/java/org/apache/sshd/git/pack/GitPackCommandTest.java
index 339170c..f6bc31d 100644
--- a/sshd-git/src/test/java/org/apache/sshd/git/pack/GitPackCommandTest.java
+++ b/sshd-git/src/test/java/org/apache/sshd/git/pack/GitPackCommandTest.java
@@ -26,6 +26,7 @@ import com.jcraft.jsch.JSch;
 
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.git.GitLocationResolver;
 import org.apache.sshd.git.transport.GitSshdSessionFactory;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.password.AcceptAllPasswordAuthenticator;
@@ -34,6 +35,7 @@ import org.apache.sshd.util.test.BaseTestSupport;
 import org.apache.sshd.util.test.JSchLogger;
 import org.apache.sshd.util.test.Utils;
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
@@ -67,18 +69,16 @@ public class GitPackCommandTest extends BaseTestSupport {
     public void testGitPack() throws Exception {
         Assume.assumeFalse("On windows this activates TortoisePlink", OsUtils.isWin32());
 
-        Path targetParent = detectTargetFolder().getParent();
         Path gitRootDir = getTempTargetRelativeFile(getClass().getSimpleName());
-
         try (SshServer sshd = setupTestServer()) {
             Path serverRootDir = gitRootDir.resolve("server");
             sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-            sshd.setCommandFactory(new GitPackCommandFactory(Utils.resolveRelativeRemotePath(targetParent, serverRootDir)));
+            sshd.setCommandFactory(new GitPackCommandFactory(GitLocationResolver.constantPath(serverRootDir)));
             sshd.start();
 
             int port = sshd.getPort();
             try {
-                Path serverDir = serverRootDir.resolve("test.git");
+                Path serverDir = serverRootDir.resolve(getCurrentTestName() + Constants.DOT_GIT_EXT);
                 Utils.deleteRecursive(serverDir);
                 Git.init().setBare(true).setDirectory(serverDir.toFile()).call();
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a3e729/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java b/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java
index 4d84722..1e5973d 100644
--- a/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java
+++ b/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java
@@ -30,6 +30,7 @@ import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.channel.ClientChannelEvent;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.git.GitLocationResolver;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 import org.apache.sshd.util.test.BaseTestSupport;
@@ -49,17 +50,10 @@ public class GitPgmCommandTest extends BaseTestSupport {
 
     @Test
     public void testGitPgm() throws Exception {
-        Path targetParent = detectTargetFolder().getParent();
         Path serverDir = getTempTargetRelativeFile(getClass().getSimpleName());
-
-        //
-        // TODO: the GitpgmCommandFactory is kept in the test tree
-        // TODO: because it's quite limited for now
-        //
-
         try (SshServer sshd = setupTestServer()) {
             sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-            sshd.setCommandFactory(new GitPgmCommandFactory(Utils.resolveRelativeRemotePath(targetParent, serverDir)));
+            sshd.setCommandFactory(new GitPgmCommandFactory(GitLocationResolver.constantPath(serverDir)));
             sshd.start();
 
             int port = sshd.getPort();


[2/2] mina-sshd git commit: [SSHD-798] Provide capability to use a user-defined ExecutorService when servicing GIT commands

Posted by lg...@apache.org.
[SSHD-798] Provide capability to use a user-defined ExecutorService when servicing GIT commands


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/e0d63503
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/e0d63503
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/e0d63503

Branch: refs/heads/master
Commit: e0d6350360f6e36c309020ae13d75683c9a500b2
Parents: 59a7fcf
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Sun Feb 18 15:09:27 2018 +0200
Committer: Lyor Goldstein <ly...@gmail.com>
Committed: Sun Feb 18 20:22:50 2018 +0200

----------------------------------------------------------------------
 README.md                                       |  13 ++
 .../sshd/server/AbstractCommandSupport.java     | 168 ++++++++++++++++++
 .../org/apache/sshd/server/scp/ScpCommand.java  |  36 ++--
 .../server/subsystem/sftp/SftpSubsystem.java    |  34 ++--
 .../java/org/apache/sshd/agent/AgentTest.java   |   6 +-
 .../sshd/client/channel/ChannelExecTest.java    |   2 +-
 .../sshd/client/session/ClientSessionTest.java  |   6 +-
 .../sshd/util/test/CommandExecutionHelper.java  |  90 +---------
 .../org/apache/sshd/util/test/EchoShell.java    |   2 +-
 .../org/apache/sshd/git/AbstractGitCommand.java | 133 ++++++++++++++
 .../sshd/git/AbstractGitCommandFactory.java     |  96 ++++++++++
 .../apache/sshd/git/pack/GitPackCommand.java    | 175 ++++---------------
 .../sshd/git/pack/GitPackCommandFactory.java    |  34 ++--
 .../sshd/git/pgm/EmbeddedCommandRunner.java     |   6 +-
 .../org/apache/sshd/git/pgm/GitPgmCommand.java  | 145 ++-------------
 .../sshd/git/pgm/GitPgmCommandFactory.java      |  35 ++--
 16 files changed, 558 insertions(+), 423 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 82e92d3..283cc85 100644
--- a/README.md
+++ b/README.md
@@ -1380,6 +1380,19 @@ See `GitPackCommandFactory` and `GitPgmCommandFactory`. These command factories
     // or any other combination ...
 ```
 
+as with all other built-in commands, the factories allow the user to provide an `ExecutorService` in order to control the spwaned threads
+for servicing the commands. If none provided, an internal single-threaded "pool" is created ad-hoc and destroyed once the command execution
+is completed (regardless of whether successful or not):
+
+
+```java
+
+    sshd.setCommandFactory(new GitPackCommandFactory(rootDir, new MyCommandFactory())
+        .withExecutorService(myService)
+        .withShutdownOnExit(false));
+
+```
+
 ## LDAP adaptors
 
 The _sshd-ldap_ artifact contains an [LdapPasswordAuthenticator](https://issues.apache.org/jira/browse/SSHD-607) and an [LdapPublicKeyAuthenticator](https://issues.apache.org/jira/browse/SSHD-608) that have been written along the same lines as the [openssh-ldap-publickey](https://github.com/AndriiGrytsenko/openssh-ldap-publickey) project. The authenticators can be easily configured to match most LDAP schemes, or alternatively serve as base classes for code that extends them and adds proprietary logic.

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/main/java/org/apache/sshd/server/AbstractCommandSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/AbstractCommandSupport.java b/sshd-core/src/main/java/org/apache/sshd/server/AbstractCommandSupport.java
new file mode 100644
index 0000000..7533157
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/AbstractCommandSupport.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
+import org.apache.sshd.common.util.threads.ThreadUtils;
+
+/**
+ * Provides a basic useful skeleton for {@link Command} executions
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractCommandSupport
+        extends AbstractLoggingBean
+        implements Command, Runnable, ExitCallback, ExecutorServiceCarrier {
+    private final String command;
+    private InputStream in;
+    private OutputStream out;
+    private OutputStream err;
+    private ExitCallback callback;
+    private Environment environment;
+    private Future<?> cmdFuture;
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
+    private boolean cbCalled;
+
+    protected AbstractCommandSupport(String command, ExecutorService executorService, boolean shutdownOnExit) {
+        this.command = command;
+
+        if (executorService == null) {
+            String poolName = GenericUtils.isEmpty(command) ? getClass().getSimpleName() : command.replace(' ', '_').replace('/', ':');
+            this.executorService = ThreadUtils.newSingleThreadExecutor(poolName);
+            this.shutdownOnExit = true;    // we always close the ad-hoc executor service
+        } else {
+            this.executorService = executorService;
+            this.shutdownOnExit = shutdownOnExit;
+        }
+    }
+
+    public String getCommand() {
+        return command;
+    }
+
+    @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    public InputStream getInputStream() {
+        return in;
+    }
+
+    @Override
+    public void setInputStream(InputStream in) {
+        this.in = in;
+    }
+
+    public OutputStream getOutputStream() {
+        return out;
+    }
+
+    @Override
+    public void setOutputStream(OutputStream out) {
+        this.out = out;
+    }
+
+    public OutputStream getErrorStream() {
+        return err;
+    }
+
+    @Override
+    public void setErrorStream(OutputStream err) {
+        this.err = err;
+    }
+
+    public ExitCallback getExitCallback() {
+        return callback;
+    }
+
+    @Override
+    public void setExitCallback(ExitCallback callback) {
+        this.callback = callback;
+    }
+
+    public Environment getEnvironment() {
+        return environment;
+    }
+
+    protected Future<?> getStartedCommandFuture() {
+        return cmdFuture;
+    }
+
+    @Override
+    public void start(Environment env) throws IOException {
+        environment = env;
+        ExecutorService executors = getExecutorService();
+        cmdFuture = executors.submit(this);
+    }
+
+    @Override
+    public void destroy() {
+        ExecutorService executors = getExecutorService();
+        if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
+            Collection<Runnable> runners = executors.shutdownNow();
+            if (log.isDebugEnabled()) {
+                log.debug("destroy() - shutdown executor service - runners count=" + runners.size());
+            }
+        }
+        this.executorService = null;
+    }
+
+    @Override
+    public void onExit(int exitValue, String exitMessage) {
+        if (cbCalled) {
+            if (log.isTraceEnabled()) {
+                log.trace("onExit({}) ignore exitValue={}, message={} - already called",
+                        this, exitValue, exitMessage);
+            }
+            return;
+        }
+
+        ExitCallback cb = getExitCallback();
+        try {
+            if (log.isDebugEnabled()) {
+                log.debug("onExit({}) exiting - value={}, message={}", this, exitValue, exitMessage);
+            }
+            cb.onExit(exitValue, exitMessage);
+        } finally {
+            cbCalled = true;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + getCommand() + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
index 8ff52c1..23d204b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
@@ -37,6 +37,7 @@ import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.session.SessionHolder;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
 import org.apache.sshd.common.util.threads.ThreadUtils;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
@@ -55,7 +56,7 @@ import org.apache.sshd.server.session.ServerSessionHolder;
 public class ScpCommand
         extends AbstractLoggingBean
         implements Command, Runnable, FileSystemAware, SessionAware,
-                   SessionHolder<Session>, ServerSessionHolder {
+                   SessionHolder<Session>, ServerSessionHolder, ExecutorServiceCarrier {
 
     protected final String name;
     protected final int sendBufferSize;
@@ -73,12 +74,13 @@ public class ScpCommand
     protected OutputStream err;
     protected ExitCallback callback;
     protected IOException error;
-    protected ExecutorService executors;
-    protected boolean shutdownExecutor;
     protected Future<?> pendingFuture;
     protected ScpTransferEventListener listener;
     protected ServerSession serverSession;
 
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
+
     /**
      * @param command         The command to be executed
      * @param executorService An {@link ExecutorService} to be used when
@@ -103,11 +105,11 @@ public class ScpCommand
 
         if (executorService == null) {
             String poolName = command.replace(' ', '_').replace('/', ':');
-            executors = ThreadUtils.newSingleThreadExecutor(poolName);
-            shutdownExecutor = true;    // we always close the ad-hoc executor service
+            this.executorService = ThreadUtils.newSingleThreadExecutor(poolName);
+            this.shutdownOnExit = true;    // we always close the ad-hoc executor service
         } else {
-            executors = executorService;
-            shutdownExecutor = shutdownOnExit;
+            this.executorService = executorService;
+            this.shutdownOnExit = shutdownOnExit;
         }
 
         if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
@@ -174,12 +176,23 @@ public class ScpCommand
                 break;
             }
         }
-        if (!optF && !optT) {
+
+        if ((!optF) && (!optT)) {
             error = new IOException("Either -f or -t option should be set for " + command);
         }
     }
 
     @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    @Override
     public Session getSession() {
         return getServerSession();
     }
@@ -226,6 +239,7 @@ public class ScpCommand
         }
 
         try {
+            ExecutorService executors = getExecutorService();
             pendingFuture = executors.submit(this);
         } catch (RuntimeException e) {    // e.g., RejectedExecutionException
             log.error("Failed (" + e.getClass().getSimpleName() + ") to start command=" + name + ": " + e.getMessage(), e);
@@ -246,14 +260,14 @@ public class ScpCommand
 
         pendingFuture = null;
 
-        if ((executors != null) && (!executors.isShutdown()) && shutdownExecutor) {
+        ExecutorService executors = getExecutorService();
+        if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
             Collection<Runnable> runners = executors.shutdownNow();
             if (log.isDebugEnabled()) {
                 log.debug("destroy() - shutdown executor service - runners count=" + runners.size());
             }
         }
-
-        executors = null;
+        this.executorService = null;
 
         try {
             fileSystem.close();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
index 5d87d8e..e2cfa5e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
@@ -59,6 +59,7 @@ import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.BufferUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
 import org.apache.sshd.common.util.threads.ThreadUtils;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
@@ -73,7 +74,7 @@ import org.apache.sshd.server.session.ServerSession;
  */
 public class SftpSubsystem
         extends AbstractSftpSubsystemHelper
-        implements Command, Runnable, SessionAware, FileSystemAware {
+        implements Command, Runnable, SessionAware, FileSystemAware, ExecutorServiceCarrier {
 
     /**
      * Properties key for the maximum of available open handles per session.
@@ -120,8 +121,6 @@ public class SftpSubsystem
     protected Random randomizer;
     protected int fileHandleSize = DEFAULT_FILE_HANDLE_SIZE;
     protected int maxFileHandleRounds = DEFAULT_FILE_HANDLE_ROUNDS;
-    protected ExecutorService executors;
-    protected boolean shutdownExecutor;
     protected Future<?> pendingFuture;
     protected byte[] workBuf = new byte[Math.max(DEFAULT_FILE_HANDLE_SIZE, Integer.BYTES)];
     protected FileSystem fileSystem = FileSystems.getDefault();
@@ -133,6 +132,8 @@ public class SftpSubsystem
 
     private ServerSession serverSession;
     private final AtomicBoolean closed = new AtomicBoolean(false);
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
 
     /**
      * @param executorService The {@link ExecutorService} to be used by
@@ -153,11 +154,11 @@ public class SftpSubsystem
         super(policy, accessor, errorStatusDataHandler);
 
         if (executorService == null) {
-            executors = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
-            shutdownExecutor = true;    // we always close the ad-hoc executor service
+            this.executorService = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
+            this.shutdownOnExit = true;    // we always close the ad-hoc executor service
         } else {
-            executors = executorService;
-            shutdownExecutor = shutdownOnExit;
+            this.executorService = executorService;
+            this.shutdownOnExit = shutdownOnExit;
         }
     }
 
@@ -172,6 +173,16 @@ public class SftpSubsystem
     }
 
     @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    @Override
     public void setSession(ServerSession session) {
         this.serverSession = Objects.requireNonNull(session, "No session");
 
@@ -233,7 +244,8 @@ public class SftpSubsystem
     public void start(Environment env) throws IOException {
         this.env = env;
         try {
-            pendingFuture = executors.submit(this);
+            ExecutorService executor = getExecutorService();
+            pendingFuture = executor.submit(this);
         } catch (RuntimeException e) {    // e.g., RejectedExecutionException
             log.error("Failed (" + e.getClass().getSimpleName() + ") to start command: " + e.toString(), e);
             throw new IOException(e);
@@ -1029,14 +1041,14 @@ public class SftpSubsystem
 
         pendingFuture = null;
 
-        if ((executors != null) && (!executors.isShutdown()) && shutdownExecutor) {
+        ExecutorService executors = getExecutorService();
+        if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
             Collection<Runnable> runners = executors.shutdownNow();
             if (log.isDebugEnabled()) {
                 log.debug("destroy(" + session + ") - shutdown executor service - runners count=" + runners.size());
             }
         }
-
-        executors = null;
+        this.executorService = null;
 
         try {
             fileSystem.close();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java b/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java
index 113dc98..f252be7 100644
--- a/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/agent/AgentTest.java
@@ -159,9 +159,9 @@ public class AgentTest extends BaseTestSupport {
                                         session2.auth().verify(15L, TimeUnit.SECONDS);
 
                                         try (ChannelShell channel2 = session2.createShellChannel()) {
-                                            channel2.setIn(shellFactory.shell.getIn());
-                                            channel2.setOut(shellFactory.shell.getOut());
-                                            channel2.setErr(shellFactory.shell.getErr());
+                                            channel2.setIn(shellFactory.shell.getInputStream());
+                                            channel2.setOut(shellFactory.shell.getOutputStream());
+                                            channel2.setErr(shellFactory.shell.getErrorStream());
                                             channel2.setAgentForwarding(true);
                                             channel2.open().verify(9L, TimeUnit.SECONDS);
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java b/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java
index 01cfed4..038ce55 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/channel/ChannelExecTest.java
@@ -54,7 +54,7 @@ public class ChannelExecTest extends BaseTestSupport {
         sshd.setCommandFactory(command -> new CommandExecutionHelper(command) {
             @Override
             protected boolean handleCommandLine(String command) throws Exception {
-                OutputStream stdout = getOut();
+                OutputStream stdout = getOutputStream();
                 stdout.write(command.getBytes(StandardCharsets.US_ASCII));
                 stdout.flush();
                 return false;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java b/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java
index 2b14a4c..aeb5e3b 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/session/ClientSessionTest.java
@@ -91,7 +91,7 @@ public class ClientSessionTest extends BaseTestSupport {
             protected boolean handleCommandLine(String command) throws Exception {
                 assertEquals("Mismatched incoming command", expectedCommand, command);
                 assertFalse("Duplicated command call", cmdProcessed);
-                OutputStream stdout = getOut();
+                OutputStream stdout = getOutputStream();
                 stdout.write(expectedResponse.getBytes(StandardCharsets.US_ASCII));
                 stdout.flush();
                 cmdProcessed = true;
@@ -120,7 +120,7 @@ public class ClientSessionTest extends BaseTestSupport {
             protected boolean handleCommandLine(String command) throws Exception {
                 assertEquals("Mismatched incoming command", expectedCommand, command);
                 assertFalse("Duplicated command call", cmdProcessed);
-                OutputStream stderr = getErr();
+                OutputStream stderr = getErrorStream();
                 stderr.write(expectedErrorMessage.getBytes(StandardCharsets.US_ASCII));
                 stderr.flush();
                 cmdProcessed = true;
@@ -171,7 +171,7 @@ public class ClientSessionTest extends BaseTestSupport {
                     protected boolean handleCommandLine(String command) throws Exception {
                         assertEquals("Mismatched incoming command", expectedCommand, command);
                         assertFalse("Duplicated command call", cmdProcessed);
-                        OutputStream stdout = getOut();
+                        OutputStream stdout = getOutputStream();
                         stdout.write(command.getBytes(StandardCharsets.US_ASCII));
                         stdout.flush();
                         cmdProcessed = true;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java b/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java
index ce47a65..c44a2ad 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java
@@ -21,97 +21,31 @@ package org.apache.sshd.util.test;
 
 import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.InterruptedIOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.AbstractCommandSupport;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public abstract class CommandExecutionHelper extends AbstractLoggingBean implements Command, Runnable, ExitCallback {
-    private InputStream in;
-    private OutputStream out;
-    private OutputStream err;
-    private ExitCallback callback;
-    private Environment environment;
-    private Thread thread;
-    private boolean cbCalled;
-    // null/empty if shell
-    private String command;
-
+public abstract class CommandExecutionHelper extends AbstractCommandSupport {
     protected CommandExecutionHelper() {
         this(null);
     }
 
     protected CommandExecutionHelper(String command) {
-        this.command = command;
-    }
-
-    public InputStream getIn() {
-        return in;
-    }
-
-    public OutputStream getOut() {
-        return out;
-    }
-
-    public OutputStream getErr() {
-        return err;
-    }
-
-    public Environment getEnvironment() {
-        return environment;
-    }
-
-    public ExitCallback getExitCallback() {
-        return callback;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        environment = env;
-        thread = new Thread(this, "CommandExecutionHelper");
-        thread.setDaemon(true);
-        thread.start();
-    }
-
-    @Override
-    public void destroy() {
-        thread.interrupt();
+        super(command, null, true);
     }
 
     @Override
     public void run() {
+        String command = getCommand();
         try {
             if (command == null) {
-                try (BufferedReader r = new BufferedReader(new InputStreamReader(getIn(), StandardCharsets.UTF_8))) {
+                try (BufferedReader r = new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8))) {
                     for (;;) {
                         command = r.readLine();
                         if (command == null) {
@@ -131,7 +65,7 @@ public abstract class CommandExecutionHelper extends AbstractLoggingBean impleme
         } catch (Exception e) {
             String message = "Failed (" + e.getClass().getSimpleName() + ") to handle '" + command + "': " + e.getMessage();
             try {
-                OutputStream stderr = getErr();
+                OutputStream stderr = getErrorStream();
                 stderr.write(message.getBytes(StandardCharsets.US_ASCII));
             } catch (IOException ioe) {
                 log.warn("Failed ({}) to write error message={}: {}",
@@ -144,18 +78,6 @@ public abstract class CommandExecutionHelper extends AbstractLoggingBean impleme
         }
     }
 
-    @Override
-    public void onExit(int exitValue, String exitMessage) {
-        if (!cbCalled) {
-            ExitCallback cb = getExitCallback();
-            try {
-                cb.onExit(exitValue, exitMessage);
-            } finally {
-                cbCalled = true;
-            }
-        }
-    }
-
     /**
      * @param command The command line
      * @return {@code true} if continue accepting command

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
index 76a8d5d..9ad8a14 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/EchoShell.java
@@ -31,7 +31,7 @@ public class EchoShell extends CommandExecutionHelper {
 
     @Override
     protected boolean handleCommandLine(String command) throws Exception {
-        OutputStream out = getOut();
+        OutputStream out = getOutputStream();
         out.write((command + "\n").getBytes(StandardCharsets.UTF_8));
         out.flush();
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
new file mode 100644
index 0000000..2ed85b0
--- /dev/null
+++ b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommand.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.git;
+
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.common.channel.ChannelOutputStream;
+import org.apache.sshd.server.AbstractCommandSupport;
+
+/**
+ * Provides basic support for GIT command implementations
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractGitCommand extends AbstractCommandSupport {
+    public static final int CHAR = 0x001;
+    public static final int DELIMITER = 0x002;
+    public static final int STARTQUOTE = 0x004;
+    public static final int ENDQUOTE = 0x008;
+
+    private String rootDir;
+
+    protected AbstractGitCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
+        super(command, executorService, shutdownOnExit);
+        this.rootDir = rootDir;
+    }
+
+    public String getRootDir() {
+        return rootDir;
+    }
+
+    @Override
+    public void setOutputStream(OutputStream out) {
+        super.setOutputStream(out);
+        if (out instanceof ChannelOutputStream) {
+            ((ChannelOutputStream) out).setNoDelay(true);
+        }
+    }
+
+    @Override
+    public void setErrorStream(OutputStream err) {
+        super.setErrorStream(err);
+        if (err instanceof ChannelOutputStream) {
+            ((ChannelOutputStream) err).setNoDelay(true);
+        }
+    }
+
+    /**
+     * Parses delimited string and returns an array containing the tokens. This
+     * parser obeys quotes, so the delimiter character will be ignored if it is
+     * inside of a quote. This method assumes that the quote character is not
+     * included in the set of delimiter characters.
+     *
+     * @param value the delimited string to parse.
+     * @param delim the characters delimiting the tokens.
+     * @param trim {@code true} if the strings are trimmed before being added to the list
+     * @return a list of string or an empty list if there are none.
+     */
+    public static List<String> parseDelimitedString(String value, String delim, boolean trim) {
+        if (value == null) {
+            value = "";
+        }
+
+        List<String> list = new ArrayList<>();
+        StringBuilder sb = new StringBuilder();
+        int expecting = CHAR | DELIMITER | STARTQUOTE;
+        boolean isEscaped = false;
+        for (int i = 0; i < value.length(); i++) {
+            char c = value.charAt(i);
+            boolean isDelimiter = delim.indexOf(c) >= 0;
+            if (!isEscaped && (c == '\\')) {
+                isEscaped = true;
+                continue;
+            }
+
+            if (isEscaped) {
+                sb.append(c);
+            } else if (isDelimiter && ((expecting & DELIMITER) != 0)) {
+                if (trim) {
+                    String str = sb.toString();
+                    list.add(str.trim());
+                } else {
+                    list.add(sb.toString());
+                }
+                sb.delete(0, sb.length());
+                expecting = CHAR | DELIMITER | STARTQUOTE;
+            } else if ((c == '"') && ((expecting & STARTQUOTE) != 0)) {
+                sb.append(c);
+                expecting = CHAR | ENDQUOTE;
+            } else if ((c == '"') && ((expecting & ENDQUOTE) != 0)) {
+                sb.append(c);
+                expecting = CHAR | STARTQUOTE | DELIMITER;
+            } else if ((expecting & CHAR) != 0) {
+                sb.append(c);
+            } else {
+                throw new IllegalArgumentException("Invalid delimited string: " + value);
+            }
+
+            isEscaped = false;
+        }
+
+        if (sb.length() > 0) {
+            if (trim) {
+                String str = sb.toString();
+                list.add(str.trim());
+            } else {
+                list.add(sb.toString());
+            }
+        }
+
+        return list;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
new file mode 100644
index 0000000..337643d
--- /dev/null
+++ b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.git;
+
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+import org.apache.sshd.server.scp.UnknownCommand;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractGitCommandFactory implements CommandFactory, ExecutorServiceCarrier {
+    private final String rootDir;
+    private final CommandFactory delegate;
+    private final String cmdPrefix;
+    private ExecutorService executorService;
+    private boolean shutdownOnExit;
+
+    protected AbstractGitCommandFactory(String rootDir, CommandFactory delegate, String cmdPrefix) {
+        this.rootDir = rootDir;
+        this.delegate = delegate;
+        this.cmdPrefix = ValidateUtils.checkNotNullAndNotEmpty(cmdPrefix, "No command prefix provided");
+    }
+
+    public AbstractGitCommandFactory withExecutorService(ExecutorService executorService) {
+        this.executorService = executorService;
+        return this;
+    }
+
+    public String getRootDir() {
+        return rootDir;
+    }
+
+    public CommandFactory getDelegate() {
+        return delegate;
+    }
+
+    public String getCommandPrefix() {
+        return cmdPrefix;
+    }
+
+    public AbstractGitCommandFactory withShutdownOnExit(boolean shutdownOnExit) {
+        this.shutdownOnExit = shutdownOnExit;
+        return this;
+    }
+
+    @Override
+    public ExecutorService getExecutorService() {
+        return executorService;
+    }
+
+    @Override
+    public boolean isShutdownOnExit() {
+        return shutdownOnExit;
+    }
+
+    @Override
+    public Command createCommand(String command) {
+        String prefix = getCommandPrefix();
+        if (command.startsWith(prefix)) {
+            return createGitCommand(command);
+        }
+
+        CommandFactory delegate = getDelegate();
+        if (delegate != null) {
+            return delegate.createCommand(command);
+        } else {
+            return new UnknownCommand(command);
+        }
+    }
+
+    protected abstract AbstractGitCommand createGitCommand(String command);
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
index 229cbfb..8aebac5 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommand.java
@@ -19,20 +19,15 @@
 package org.apache.sshd.git.pack;
 
 import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 
-import org.apache.sshd.common.channel.ChannelOutputStream;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.server.Command;
+import org.apache.sshd.git.AbstractGitCommand;
 import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.util.FS;
 
@@ -41,166 +36,56 @@ import org.eclipse.jgit.util.FS;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class GitPackCommand extends AbstractLoggingBean implements Command, Runnable {
-
-    private static final int CHAR = 1;
-    private static final int DELIMITER = 2;
-    private static final int STARTQUOTE = 4;
-    private static final int ENDQUOTE = 8;
-
-    private String rootDir;
-    private String command;
-    private InputStream in;
-    private OutputStream out;
-    private OutputStream err;
-    private ExitCallback callback;
-
-    public GitPackCommand(String rootDir, String command) {
-        this.rootDir = rootDir;
-        this.command = command;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-        if (out instanceof ChannelOutputStream) {
-            ((ChannelOutputStream) out).setNoDelay(true);
-        }
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-        if (err instanceof ChannelOutputStream) {
-            ((ChannelOutputStream) err).setNoDelay(true);
-        }
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        Thread thread = new Thread(this);
-        thread.setDaemon(true);
-        thread.start();
+public class GitPackCommand extends AbstractGitCommand {
+    /**
+     * @param rootDir Root directory for the command
+     * @param command Command to execute
+     * @param executorService An {@link ExecutorService} to be used when {@link #start(Environment)}-ing
+     * execution. If {@code null} an ad-hoc single-threaded service is created and used.
+     * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()} will be called when
+     * command terminates - unless it is the ad-hoc service, which will be shutdown regardless
+     */
+    public GitPackCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
+        super(rootDir, command, executorService, shutdownOnExit);
     }
 
     @Override
     public void run() {
+        String command = getCommand();
         try {
             List<String> strs = parseDelimitedString(command, " ", true);
             String[] args = strs.toArray(new String[strs.size()]);
             for (int i = 0; i < args.length; i++) {
-                if (args[i].startsWith("'") && args[i].endsWith("'")) {
-                    args[i] = args[i].substring(1, args[i].length() - 1);
+                String argVal = args[i];
+                if (argVal.startsWith("'") && argVal.endsWith("'")) {
+                    args[i] = argVal.substring(1, argVal.length() - 1);
+                    argVal = args[i];
                 }
-                if (args[i].startsWith("\"") && args[i].endsWith("\"")) {
-                    args[i] = args[i].substring(1, args[i].length() - 1);
+                if (argVal.startsWith("\"") && argVal.endsWith("\"")) {
+                    args[i] = argVal.substring(1, argVal.length() - 1);
+                    argVal = args[i];
                 }
             }
 
             if (args.length != 2) {
                 throw new IllegalArgumentException("Invalid git command line: " + command);
             }
+
+            String rootDir = getRootDir();
             File srcGitdir = new File(rootDir, args[1]);
             RepositoryCache.FileKey key = RepositoryCache.FileKey.lenient(srcGitdir, FS.DETECTED);
             Repository db = key.open(true /* must exist */);
-            if ("git-upload-pack".equals(args[0])) {
-                new UploadPack(db).upload(in, out, err);
-            } else if ("git-receive-pack".equals(args[0])) {
-                new ReceivePack(db).receive(in, out, err);
+            if (RemoteConfig.DEFAULT_UPLOAD_PACK.equals(args[0])) {
+                new UploadPack(db).upload(getInputStream(), getOutputStream(), getErrorStream());
+            } else if (RemoteConfig.DEFAULT_RECEIVE_PACK.equals(args[0])) {
+                new ReceivePack(db).receive(getInputStream(), getOutputStream(), getErrorStream());
             } else {
                 throw new IllegalArgumentException("Unknown git command: " + command);
             }
 
-            if (callback != null) {
-                callback.onExit(0);
-            }
+            onExit(0);
         } catch (Throwable t) {
-            log.warn("Failed {} to execute command={}: {}",
-                     t.getClass().getSimpleName(), command, t.getMessage());
-            if (callback != null) {
-                callback.onExit(-1, t.getClass().getSimpleName());
-            }
-        }
-    }
-
-    @Override
-    public void destroy() {
-        //To change body of implemented methods use File | Settings | File Templates.
-    }
-
-    /**
-     * Parses delimited string and returns an array containing the tokens. This
-     * parser obeys quotes, so the delimiter character will be ignored if it is
-     * inside of a quote. This method assumes that the quote character is not
-     * included in the set of delimiter characters.
-     *
-     * @param value the delimited string to parse.
-     * @param delim the characters delimiting the tokens.
-     * @param trim {@code true} if the strings are trimmed before being added to the list
-     * @return a list of string or an empty list if there are none.
-     */
-    private static List<String> parseDelimitedString(String value, String delim, boolean trim) {
-        if (value == null) {
-            value = "";
+            onExit(-1, t.getClass().getSimpleName());
         }
-
-        List<String> list = new ArrayList<>();
-        StringBuilder sb = new StringBuilder();
-        int expecting = CHAR | DELIMITER | STARTQUOTE;
-        boolean isEscaped = false;
-        for (int i = 0; i < value.length(); i++) {
-            char c = value.charAt(i);
-            boolean isDelimiter = delim.indexOf(c) >= 0;
-            if (!isEscaped && (c == '\\')) {
-                isEscaped = true;
-                continue;
-            }
-
-            if (isEscaped) {
-                sb.append(c);
-            } else if (isDelimiter && ((expecting & DELIMITER) != 0)) {
-                if (trim) {
-                    String str = sb.toString();
-                    list.add(str.trim());
-                } else {
-                    list.add(sb.toString());
-                }
-                sb.delete(0, sb.length());
-                expecting = CHAR | DELIMITER | STARTQUOTE;
-            } else if ((c == '"') && ((expecting & STARTQUOTE) != 0)) {
-                sb.append(c);
-                expecting = CHAR | ENDQUOTE;
-            } else if ((c == '"') && ((expecting & ENDQUOTE) != 0)) {
-                sb.append(c);
-                expecting = CHAR | STARTQUOTE | DELIMITER;
-            } else if ((expecting & CHAR) != 0) {
-                sb.append(c);
-            } else {
-                throw new IllegalArgumentException("Invalid delimited string: " + value);
-            }
-
-            isEscaped = false;
-        }
-
-        if (sb.length() > 0) {
-            if (trim) {
-                String str = sb.toString();
-                list.add(str.trim());
-            } else {
-                list.add(sb.toString());
-            }
-        }
-
-        return list;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
index b5399c5..e12c228 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pack/GitPackCommandFactory.java
@@ -18,37 +18,39 @@
  */
 package org.apache.sshd.git.pack;
 
-import org.apache.sshd.server.Command;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.git.AbstractGitCommandFactory;
 import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.UnknownCommand;
 
 /**
  * TODO Add javadoc
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class GitPackCommandFactory implements CommandFactory {
-
-    private final String rootDir;
-    private final CommandFactory delegate;
+public class GitPackCommandFactory extends AbstractGitCommandFactory {
+    public static final String GIT_COMMAND_PREFIX = "git-";
 
     public GitPackCommandFactory(String rootDir) {
         this(rootDir,  null);
     }
 
     public GitPackCommandFactory(String rootDir, CommandFactory delegate) {
-        this.rootDir = rootDir;
-        this.delegate = delegate;
+        super(rootDir, delegate, GIT_COMMAND_PREFIX);
+    }
+
+    @Override
+    public GitPackCommandFactory withExecutorService(ExecutorService executorService) {
+        return (GitPackCommandFactory) super.withExecutorService(executorService);
+    }
+
+    @Override
+    public GitPackCommandFactory withShutdownOnExit(boolean shutdownOnExit) {
+        return (GitPackCommandFactory) super.withShutdownOnExit(shutdownOnExit);
     }
 
     @Override
-    public Command createCommand(String command) {
-        if (command.startsWith("git-")) {
-            return new GitPackCommand(rootDir, command);
-        } else if (delegate != null) {
-            return delegate.createCommand(command);
-        } else {
-            return new UnknownCommand(command);
-        }
+    public GitPackCommand createGitCommand(String command) {
+        return new GitPackCommand(getRootDir(), command, getExecutorService(), isShutdownOnExit());
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
index 8ccc510..204dc80 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/EmbeddedCommandRunner.java
@@ -81,8 +81,8 @@ public class EmbeddedCommandRunner {
      * @param err the error stream, may be null in which case the system error stream will be used
      * @throws Exception if an error occurs
      */
-    public void execute(final String[] argv, InputStream in, OutputStream out, OutputStream err) throws Exception {
-        final CmdLineParser clp = new CmdLineParser(this);
+    public void execute(String[] argv, InputStream in, OutputStream out, OutputStream err) throws Exception {
+        CmdLineParser clp = new CmdLineParser(this);
         PrintWriter writer = new PrintWriter(err != null ? err : System.err);
         try {
             clp.parseArgument(argv);
@@ -95,7 +95,7 @@ public class EmbeddedCommandRunner {
         }
 
         if (argv.length == 0 || help) {
-            final String ex = clp.printExample(OptionHandlerFilter.ALL, CLIText.get().resourceBundle());
+            String ex = clp.printExample(OptionHandlerFilter.ALL, CLIText.get().resourceBundle());
             writer.println("jgit" + ex + " command [ARG ...]"); //$NON-NLS-1$
             if (help) {
                 writer.println();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
index 013f4e5..2b7a1ab 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommand.java
@@ -19,15 +19,12 @@
 package org.apache.sshd.git.pgm;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 
-import org.apache.sshd.common.channel.ChannelOutputStream;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.server.Command;
+import org.apache.sshd.git.AbstractGitCommand;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
 
@@ -36,60 +33,24 @@ import org.apache.sshd.server.ExitCallback;
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class GitPgmCommand extends AbstractLoggingBean implements Command, Runnable {
-
-    private static final int CHAR = 1;
-    private static final int DELIMITER = 2;
-    private static final int STARTQUOTE = 4;
-    private static final int ENDQUOTE = 8;
-
-    private String rootDir;
-    private String command;
-    private InputStream in;
-    private OutputStream out;
-    private OutputStream err;
-    private ExitCallback callback;
-
-    public GitPgmCommand(String rootDir, String command) {
-        this.rootDir = rootDir;
-        this.command = command;
-    }
-
-    @Override
-    public void setInputStream(InputStream in) {
-        this.in = in;
-    }
-
-    @Override
-    public void setOutputStream(OutputStream out) {
-        this.out = out;
-        if (out instanceof ChannelOutputStream) {
-            ((ChannelOutputStream) out).setNoDelay(true);
-        }
-    }
-
-    @Override
-    public void setErrorStream(OutputStream err) {
-        this.err = err;
-        if (err instanceof ChannelOutputStream) {
-            ((ChannelOutputStream) err).setNoDelay(true);
-        }
-    }
-
-    @Override
-    public void setExitCallback(ExitCallback callback) {
-        this.callback = callback;
-    }
-
-    @Override
-    public void start(Environment env) throws IOException {
-        Thread thread = new Thread(this);
-        thread.setDaemon(true);
-        thread.start();
+public class GitPgmCommand extends AbstractGitCommand {
+    /**
+     * @param rootDir Root directory for the command
+     * @param command Command to execute
+     * @param executorService An {@link ExecutorService} to be used when {@link #start(Environment)}-ing
+     * execution. If {@code null} an ad-hoc single-threaded service is created and used.
+     * @param shutdownOnExit  If {@code true} the {@link ExecutorService#shutdownNow()} will be called when
+     * command terminates - unless it is the ad-hoc service, which will be shutdown regardless
+     */
+    public GitPgmCommand(String rootDir, String command, ExecutorService executorService, boolean shutdownOnExit) {
+        super(rootDir, command, executorService, shutdownOnExit);
     }
 
     @Override
     public void run() {
+        String command = getCommand();
+        ExitCallback callback = getExitCallback();
+        OutputStream err = getErrorStream();
         try {
             List<String> strs = parseDelimitedString(command, " ", true);
             String[] args = strs.toArray(new String[strs.size()]);
@@ -102,7 +63,7 @@ public class GitPgmCommand extends AbstractLoggingBean implements Command, Runna
                 }
             }
 
-            new EmbeddedCommandRunner(rootDir).execute(args, in, out, err);
+            new EmbeddedCommandRunner(getRootDir()).execute(args, getInputStream(), getOutputStream(), err);
             if (callback != null) {
                 callback.onExit(0);
             }
@@ -119,76 +80,4 @@ public class GitPgmCommand extends AbstractLoggingBean implements Command, Runna
             }
         }
     }
-
-    @Override
-    public void destroy() {
-        //To change body of implemented methods use File | Settings | File Templates.
-    }
-
-    /**
-     * Parses delimited string and returns an array containing the tokens. This
-     * parser obeys quotes, so the delimiter character will be ignored if it is
-     * inside of a quote. This method assumes that the quote character is not
-     * included in the set of delimiter characters.
-     *
-     * @param value the delimited string to parse.
-     * @param delim the characters delimiting the tokens.
-     * @param trim {@code true} if the strings are trimmed before being added to the list
-     * @return a list of string or an empty list if there are none.
-     */
-    private static List<String> parseDelimitedString(String value, String delim, boolean trim) {
-        if (value == null) {
-            value = "";
-        }
-
-        List<String> list = new ArrayList<>();
-        StringBuilder sb = new StringBuilder();
-        int expecting = CHAR | DELIMITER | STARTQUOTE;
-        boolean isEscaped = false;
-        for (int i = 0; i < value.length(); i++) {
-            char c = value.charAt(i);
-            boolean isDelimiter = delim.indexOf(c) >= 0;
-
-            if (!isEscaped && (c == '\\')) {
-                isEscaped = true;
-                continue;
-            }
-
-            if (isEscaped) {
-                sb.append(c);
-            } else if (isDelimiter && ((expecting & DELIMITER) != 0)) {
-                if (trim) {
-                    String str = sb.toString();
-                    list.add(str.trim());
-                } else {
-                    list.add(sb.toString());
-                }
-                sb.delete(0, sb.length());
-                expecting = CHAR | DELIMITER | STARTQUOTE;
-            } else if ((c == '"') && ((expecting & STARTQUOTE) != 0)) {
-                sb.append(c);
-                expecting = CHAR | ENDQUOTE;
-            } else if ((c == '"') && ((expecting & ENDQUOTE) != 0)) {
-                sb.append(c);
-                expecting = CHAR | STARTQUOTE | DELIMITER;
-            } else if ((expecting & CHAR) != 0) {
-                sb.append(c);
-            } else {
-                throw new IllegalArgumentException("Invalid delimited string: " + value);
-            }
-
-            isEscaped = false;
-        }
-
-        if (sb.length() > 0) {
-            if (trim) {
-                String str = sb.toString();
-                list.add(str.trim());
-            } else {
-                list.add(sb.toString());
-            }
-        }
-
-        return list;
-    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e0d63503/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
index 8323528..cba6c4e 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/pgm/GitPgmCommandFactory.java
@@ -18,38 +18,39 @@
  */
 package org.apache.sshd.git.pgm;
 
-import org.apache.sshd.server.Command;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.git.AbstractGitCommandFactory;
 import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.UnknownCommand;
 
 /**
- * TODO Add javadoc
+ * Runs a GIT command locally using an embedded executor
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class GitPgmCommandFactory implements CommandFactory {
+public class GitPgmCommandFactory extends AbstractGitCommandFactory {
     public static final String GIT_COMMAND_PREFIX = "git ";
 
-    private final String rootDir;
-    private final CommandFactory delegate;
-
     public GitPgmCommandFactory(String rootDir) {
         this(rootDir,  null);
     }
 
     public GitPgmCommandFactory(String rootDir, CommandFactory delegate) {
-        this.rootDir = rootDir;
-        this.delegate = delegate;
+        super(rootDir, delegate, GIT_COMMAND_PREFIX);
+    }
+
+    @Override
+    public GitPgmCommandFactory withExecutorService(ExecutorService executorService) {
+        return (GitPgmCommandFactory) super.withExecutorService(executorService);
+    }
+
+    @Override
+    public GitPgmCommandFactory withShutdownOnExit(boolean shutdownOnExit) {
+        return (GitPgmCommandFactory) super.withShutdownOnExit(shutdownOnExit);
     }
 
     @Override
-    public Command createCommand(String command) {
-        if (command.startsWith(GIT_COMMAND_PREFIX)) {
-            return new GitPgmCommand(rootDir, command.substring(GIT_COMMAND_PREFIX.length()));
-        } else if (delegate != null) {
-            return delegate.createCommand(command);
-        } else {
-            return new UnknownCommand(command);
-        }
+    public GitPgmCommand createGitCommand(String command) {
+        return new GitPgmCommand(getRootDir(), command.substring(GIT_COMMAND_PREFIX.length()), getExecutorService(), isShutdownOnExit());
     }
 }