David Ostrovsky
2014-02-22 e3b636e7fa2a823cfe90ea75e88034a60f7e59e6
SSHD: Add support for git pack commands

Add git-upload-pack and git-receive-pack commands.

Conflicts:
src/main/java/com/gitblit/manager/ServicesManager.java
src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java
src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java

Change-Id: I8c057b41f1dfad6d004e6aa91f96c8c673be9be2
4 files added
13 files modified
544 ■■■■■ changed files
src/main/java/com/gitblit/Constants.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/GitblitReceivePackFactory.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/RepositoryResolver.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/AuthenticationManager.java 29 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/GitblitManager.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/IAuthenticationManager.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/ServicesManager.java 1 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java 108 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java 147 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/SshContext.java 35 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/SshDaemon.java 24 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/SshSession.java 9 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java 38 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java 48 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/Receive.java 34 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/Upload.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/Constants.java
@@ -501,7 +501,7 @@
    }
    public static enum AuthenticationType {
        CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;
        SSH, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;
        public boolean isStandard() {
            return ordinal() <= COOKIE.ordinal();
src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
@@ -31,6 +31,7 @@
import com.gitblit.manager.IGitblit;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.SshSession;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.StringUtils;
@@ -88,6 +89,13 @@
            // set timeout from Git daemon
            timeout = client.getDaemon().getTimeout();
        } else if (req instanceof SshSession) {
            // SSH request is always authenticated
            SshSession s = (SshSession) req;
            repositoryName = s.getRepositoryName();
            origin = s.getRemoteAddress().toString();
            String username = s.getRemoteUser();
            user = gitblit.getUserModel(username);
        }
        boolean allowAnonymousPushes = settings.getBoolean(Keys.git.allowAnonymousPushes, false);
src/main/java/com/gitblit/git/RepositoryResolver.java
@@ -30,6 +30,7 @@
import com.gitblit.manager.IGitblit;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.SshSession;
/**
 * Resolves repositories and grants export access.
@@ -67,6 +68,9 @@
            // git request
            GitDaemonClient client = (GitDaemonClient) req;
            client.setRepositoryName(name);
        } else if (req instanceof SshSession) {
            SshSession s = (SshSession)req;
            s.setRepositoryName(name);
        }
        return repo;
    }
@@ -98,6 +102,12 @@
            if (user == null) {
                user = UserModel.ANONYMOUS;
            }
        } else if (req instanceof SshSession) {
            SshSession s = (SshSession) req;
            user = gitblit.authenticate(s);
            if (user == null) {
                throw new IOException(String.format("User %s not found",  s.getRemoteUser()));
            }
        }
        if (user.canClone(model)) {
src/main/java/com/gitblit/manager/AuthenticationManager.java
@@ -47,6 +47,7 @@
import com.gitblit.auth.WindowsAuthProvider;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.SshSession;
import com.gitblit.utils.Base64;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.StringUtils;
@@ -290,6 +291,34 @@
    }
    /**
     * Authenticate a user based on SSH session.
     *
     * @param SshSession
     * @return a user object or null
     */
    @Override
    public UserModel authenticate(SshSession sshSession) {
        String username = sshSession.getRemoteUser();
        if (username != null) {
            if (!StringUtils.isEmpty(username)) {
                UserModel user = userManager.getUserModel(username);
                if (user != null) {
                    // existing user
                    logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}",
                            user.username, sshSession.getRemoteAddress()));
                    return validateAuthentication(user, AuthenticationType.SSH);
                }
                logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted ssh authentication from {1}",
                            username, sshSession.getRemoteAddress()));
            }
        } else {
            logger.warn("Empty user in SSH session");
        }
        return null;
    }
    /**
     * This method allows the authentication manager to reject authentication
     * attempts.  It is called after the username/secret have been verified to
     * ensure that the authentication technique has been logged.
src/main/java/com/gitblit/manager/GitblitManager.java
@@ -68,6 +68,7 @@
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.tickets.ITicketService;
import com.gitblit.transport.ssh.SshSession;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.JsonUtils;
@@ -651,6 +652,12 @@
        }
        return user;
    }
    @Override
    public UserModel authenticate(SshSession sshSession) {
        return authenticationManager.authenticate(sshSession);
    }
    @Override
    public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) {
        UserModel user = authenticationManager.authenticate(httpRequest, requiresCertificate);
src/main/java/com/gitblit/manager/IAuthenticationManager.java
@@ -20,6 +20,7 @@
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.SshSession;
public interface IAuthenticationManager extends IManager {
@@ -33,6 +34,8 @@
     */
    UserModel authenticate(HttpServletRequest httpRequest);
    public UserModel authenticate(SshSession sshSession);
    /**
     * Authenticate a user based on HTTP request parameters.
     *
src/main/java/com/gitblit/manager/ServicesManager.java
@@ -247,6 +247,5 @@
                    "Next pull of {0} @ {1} scheduled for {2,date,yyyy-MM-dd HH:mm}",
                    registration.name, registration.url, registration.nextPull));
        }
    }
}
src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java
New file
@@ -0,0 +1,108 @@
/*
 * Copyright 2014 gitblit.com.
 *
 * Licensed 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 com.gitblit.transport.ssh;
import java.io.IOException;
import org.apache.sshd.server.Environment;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.UploadPackFactory;
import org.kohsuke.args4j.Argument;
import com.gitblit.git.GitblitReceivePackFactory;
import com.gitblit.git.GitblitUploadPackFactory;
import com.gitblit.git.RepositoryResolver;
import com.gitblit.transport.ssh.commands.BaseCommand;
/**
 * @author Eric Myhre
 *
 */
public abstract class AbstractGitCommand extends BaseCommand {
    @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name")
    protected String repository;
    protected RepositoryResolver<SshSession> repositoryResolver;
    protected ReceivePackFactory<SshSession> receivePackFactory;
    protected UploadPackFactory<SshSession> uploadPackFactory;
    protected Repository repo;
    @Override
    public void start(final Environment env) {
        startThread(new RepositoryCommandRunnable() {
            @Override
            public void run() throws Exception {
                parseCommandLine();
                AbstractGitCommand.this.service();
            }
            @Override
            public String getRepository() {
                return repository;
            }
        });
    }
    private void service() throws IOException, Failure {
        try {
            repo = openRepository();
            runImpl();
        } finally {
            if (repo != null) {
                repo.close();
            }
        }
    }
    protected abstract void runImpl() throws IOException, Failure;
    protected Repository openRepository() throws Failure {
        // Assume any attempt to use \ was by a Windows client
        // and correct to the more typical / used in Git URIs.
        //
        repository = repository.replace('\\', '/');
        // ssh://git@thishost/path should always be name="/path" here
        //
        if (!repository.startsWith("/")) {
            throw new Failure(1, "fatal: '" + repository
                    + "': not starts with / character");
        }
        repository = repository.substring(1);
        try {
            return repositoryResolver.open(ctx.getSession(), repository);
        } catch (Exception e) {
            throw new Failure(1, "fatal: '" + repository
                    + "': not a git archive", e);
        }
    }
    public void setRepositoryResolver(
            RepositoryResolver<SshSession> repositoryResolver) {
        this.repositoryResolver = repositoryResolver;
    }
    public void setReceivePackFactory(
            GitblitReceivePackFactory<SshSession> receivePackFactory) {
        this.receivePackFactory = receivePackFactory;
    }
    public void setUploadPackFactory(
            GitblitUploadPackFactory<SshSession> uploadPackFactory) {
        this.uploadPackFactory = uploadPackFactory;
    }
}
src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
@@ -31,20 +31,9 @@
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.session.ServerSession;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.PacketLineOut;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.ServiceMayNotContinueException;
import org.eclipse.jgit.transport.UploadPack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import org.eclipse.jgit.transport.resolver.UploadPackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.git.RepositoryResolver;
import com.gitblit.transport.ssh.commands.DispatchCommand;
import com.gitblit.utils.WorkQueue;
import com.google.common.util.concurrent.Atomics;
@@ -57,23 +46,13 @@
public class SshCommandFactory implements CommandFactory {
  private static final Logger logger = LoggerFactory
      .getLogger(SshCommandFactory.class);
  private RepositoryResolver<SshSession> repositoryResolver;
  private UploadPackFactory<SshSession> uploadPackFactory;
  private ReceivePackFactory<SshSession> receivePackFactory;
  private final ScheduledExecutorService startExecutor;
  private DispatchCommand dispatcher;
    public SshCommandFactory(RepositoryResolver<SshSession> repositoryResolver,
        UploadPackFactory<SshSession> uploadPackFactory,
        ReceivePackFactory<SshSession> receivePackFactory,
    public SshCommandFactory(
        WorkQueue workQueue,
        DispatchCommand d) {
        this.repositoryResolver = repositoryResolver;
        this.uploadPackFactory = uploadPackFactory;
        this.receivePackFactory = receivePackFactory;
        this.dispatcher = d;
        int threads = 2;//cfg.getInt("sshd","commandStartThreads", 2);
        startExecutor = workQueue.createQueue(threads, "SshCommandStart");
@@ -82,35 +61,34 @@
    @Override
    public Command createCommand(final String commandLine) {
      return new Trampoline(commandLine);
        /*
        if ("git-upload-pack".equals(command))
            return new UploadPackCommand(argument);
        if ("git-receive-pack".equals(command))
            return new ReceivePackCommand(argument);
        return new NonCommand();
        */
    }
      private class Trampoline implements Command, SessionAware {
        private final String[] argv;
        private ServerSession session;
        private InputStream in;
        private OutputStream out;
        private OutputStream err;
        private ExitCallback exit;
        private Environment env;
        private String cmdLine;
        private DispatchCommand cmd;
        private final AtomicBoolean logged;
        private final AtomicReference<Future<?>> task;
        Trampoline(final String cmdLine) {
          argv = split(cmdLine);
        Trampoline(String line) {
          if (line.startsWith("git-")) {
            line = "git " + line;
          }
          cmdLine = line;
          argv = split(line);
          logged = new AtomicBoolean();
          task = Atomics.newReference();
        }
        @Override
        public void setSession(ServerSession session) {
        // TODO Auto-generated method stub
          this.session = session;
        }
        @Override
@@ -148,18 +126,18 @@
            @Override
            public String toString() {
              //return "start (user " + ctx.getSession().getUsername() + ")";
              return "start (user TODO)";
              return "start (user " + session.getUsername() + ")";
            }
          }));
        }
        private void onStart() throws IOException {
          synchronized (this) {
            //final Context old = sshScope.set(ctx);
            SshContext ctx = new SshContext(session.getAttribute(SshSession.KEY), cmdLine);
            try {
              cmd = dispatcher;
              cmd.setArguments(argv);
              cmd.setContext(ctx);
              cmd.setInputStream(in);
              cmd.setOutputStream(out);
              cmd.setErrorStream(err);
@@ -178,7 +156,7 @@
              });
              cmd.start(env);
            } finally {
              //sshScope.set(old);
              ctx = null;
            }
          }
        }
@@ -286,101 +264,4 @@
        }
        return list.toArray(new String[list.size()]);
      }
    public abstract class RepositoryCommand extends AbstractSshCommand {
        protected final String repositoryName;
        public RepositoryCommand(String repositoryName) {
            this.repositoryName = repositoryName;
        }
        @Override
        public void start(Environment env) throws IOException {
            Repository db = null;
            try {
                SshSession client = session.getAttribute(SshSession.KEY);
                db = selectRepository(client, repositoryName);
                if (db == null) return;
                run(client, db);
                exit.onExit(0);
            } catch (ServiceNotEnabledException e) {
                // Ignored. Client cannot use this repository.
            } catch (ServiceNotAuthorizedException e) {
                // Ignored. Client cannot use this repository.
            } finally {
                if (db != null)
                    db.close();
                exit.onExit(1);
            }
        }
        protected Repository selectRepository(SshSession client, String name) throws IOException {
            try {
                return openRepository(client, name);
            } catch (ServiceMayNotContinueException e) {
                // An error when opening the repo means the client is expecting a ref
                // advertisement, so use that style of error.
                PacketLineOut pktOut = new PacketLineOut(out);
                pktOut.writeString("ERR " + e.getMessage() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
                return null;
            }
        }
        protected Repository openRepository(SshSession client, String name)
                throws ServiceMayNotContinueException {
            // Assume any attempt to use \ was by a Windows client
            // and correct to the more typical / used in Git URIs.
            //
            name = name.replace('\\', '/');
            // ssh://git@thishost/path should always be name="/path" here
            //
            if (!name.startsWith("/")) //$NON-NLS-1$
                return null;
            try {
                return repositoryResolver.open(client, name.substring(1));
            } catch (RepositoryNotFoundException e) {
                // null signals it "wasn't found", which is all that is suitable
                // for the remote client to know.
                return null;
            } catch (ServiceNotEnabledException e) {
                // null signals it "wasn't found", which is all that is suitable
                // for the remote client to know.
                return null;
            }
        }
        protected abstract void run(SshSession client, Repository db)
            throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException;
    }
    public class UploadPackCommand extends RepositoryCommand {
        public UploadPackCommand(String repositoryName) { super(repositoryName); }
        @Override
        protected void run(SshSession client, Repository db)
                throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException {
            UploadPack up = uploadPackFactory.create(client, db);
            up.upload(in, out, null);
        }
    }
    public class ReceivePackCommand extends RepositoryCommand {
        public ReceivePackCommand(String repositoryName) { super(repositoryName); }
        @Override
        protected void run(SshSession client, Repository db)
                throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException {
            ReceivePack rp = receivePackFactory.create(client, db);
            rp.receive(in, out, null);
        }
    }
    public static class NonCommand extends AbstractSshCommand {
        @Override
        public void start(Environment env) {
            exit.onExit(127);
        }
    }
}
src/main/java/com/gitblit/transport/ssh/SshContext.java
New file
@@ -0,0 +1,35 @@
/*
 * Copyright 2014 gitblit.com.
 *
 * Licensed 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 com.gitblit.transport.ssh;
public class SshContext {
    private final SshSession session;
    private final String commandLine;
    public SshContext(SshSession session, String commandLine) {
        this.session = session;
        this.commandLine = commandLine;
    }
    public SshSession getSession() {
        return session;
    }
    public String getCommandLine() {
        return commandLine;
    }
}
src/main/java/com/gitblit/transport/ssh/SshDaemon.java
@@ -35,6 +35,8 @@
import com.gitblit.manager.IGitblit;
import com.gitblit.transport.ssh.commands.CreateRepository;
import com.gitblit.transport.ssh.commands.DispatchCommand;
import com.gitblit.transport.ssh.commands.Receive;
import com.gitblit.transport.ssh.commands.Upload;
import com.gitblit.transport.ssh.commands.VersionCommand;
import com.gitblit.utils.IdGenerator;
import com.gitblit.utils.StringUtils;
@@ -65,9 +67,6 @@
    @SuppressWarnings("unused")
    private final IGitblit gitblit;
    private final IdGenerator idGenerator;
    private final SshServer sshd;
    /**
@@ -77,7 +76,6 @@
     */
    public SshDaemon(IGitblit gitblit, IdGenerator idGenerator) {
        this.gitblit = gitblit;
        this.idGenerator = idGenerator;
        IStoredSettings settings = gitblit.getSettings();
        int port = settings.getInteger(Keys.git.sshPort, 0);
@@ -106,15 +104,21 @@
        gitblitCmd.registerCommand(CreateRepository.class);
        gitblitCmd.registerCommand(VersionCommand.class);
        DispatchCommand dispatcher = new DispatchCommand();
        dispatcher.registerDispatcher("gitblit", gitblitCmd);
        DispatchCommand gitCmd = new DispatchCommand();
        gitCmd.registerCommand(Upload.class);
        gitCmd.registerCommand(Receive.class);
        DispatchCommand root = new DispatchCommand();
        root.registerDispatcher("gitblit", gitblitCmd);
        root.registerDispatcher("git", gitCmd);
        root.setRepositoryResolver(new RepositoryResolver<SshSession>(gitblit));
        root.setUploadPackFactory(new GitblitUploadPackFactory<SshSession>(gitblit));
        root.setReceivePackFactory(new GitblitReceivePackFactory<SshSession>(gitblit));
        SshCommandFactory commandFactory = new SshCommandFactory(
                new RepositoryResolver<SshSession>(gitblit),
                new GitblitUploadPackFactory<SshSession>(gitblit),
                new GitblitReceivePackFactory<SshSession>(gitblit),
                new WorkQueue(idGenerator),
                dispatcher);
                root);
        sshd.setCommandFactory(commandFactory);
src/main/java/com/gitblit/transport/ssh/SshSession.java
@@ -36,6 +36,7 @@
  private volatile String username;
  private volatile String authError;
  private volatile String repositoryName;
  SshSession(int sessionId, SocketAddress peer) {
    this.sessionId = sessionId;
@@ -78,6 +79,14 @@
    authError = error;
  }
  public void setRepositoryName(String repositoryName) {
    this.repositoryName = repositoryName;
  }
  public String getRepositoryName() {
    return repositoryName;
  }
  /** @return {@code true} if the authentication did not succeed. */
  boolean isAuthenticationError() {
    return authError != null;
src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
@@ -33,8 +33,10 @@
import org.slf4j.LoggerFactory;
import com.gitblit.transport.ssh.AbstractSshCommand;
import com.gitblit.transport.ssh.SshContext;
import com.gitblit.utils.IdGenerator;
import com.gitblit.utils.WorkQueue;
import com.gitblit.utils.WorkQueue.CancelableRunnable;
import com.gitblit.utils.cli.CmdLineParser;
import com.google.common.base.Charsets;
import com.google.common.util.concurrent.Atomics;
@@ -49,6 +51,9 @@
  /** Unparsed command line options. */
  private String[] argv;
  /** Ssh context */
  protected SshContext ctx;
  /** The task, as scheduled on a worker thread. */
  private final AtomicReference<Future<?>> task;
@@ -59,6 +64,10 @@
    IdGenerator gen = new IdGenerator();
    WorkQueue w = new WorkQueue(gen);
    this.executor = w.getDefaultQueue();
  }
  public void setContext(SshContext ctx) {
    this.ctx = ctx;
  }
  public void setInputStream(final InputStream in) {
@@ -77,7 +86,10 @@
    this.exit = callback;
  }
  protected void provideStateTo(final Command cmd) {
  protected void provideBaseStateTo(final Command cmd) {
    if (cmd instanceof BaseCommand) {
      ((BaseCommand)cmd).setContext(ctx);
    }
    cmd.setInputStream(in);
    cmd.setOutputStream(out);
    cmd.setErrorStream(err);
@@ -155,31 +167,25 @@
    return "";
  }
  private final class TaskThunk implements com.gitblit.utils.WorkQueue.CancelableRunnable {
  private final class TaskThunk implements CancelableRunnable {
    private final CommandRunnable thunk;
    private final String taskName;
    private TaskThunk(final CommandRunnable thunk) {
      this.thunk = thunk;
      // TODO
//      StringBuilder m = new StringBuilder("foo");
//      m.append(context.getCommandLine());
//      if (userProvider.get().isIdentifiedUser()) {
//        IdentifiedUser u = (IdentifiedUser) userProvider.get();
//        m.append(" (").append(u.getAccount().getUserName()).append(")");
//      }
      this.taskName = "foo";//m.toString();
      StringBuilder m = new StringBuilder();
      m.append(ctx.getCommandLine());
      this.taskName = m.toString();
    }
    @Override
    public void cancel() {
      synchronized (this) {
        //final Context old = sshScope.set(context);
        try {
          //onExit(/*STATUS_CANCEL*/);
        } finally {
          //sshScope.set(old);
          ctx = null;
        }
      }
    }
@@ -190,11 +196,8 @@
        final Thread thisThread = Thread.currentThread();
        final String thisName = thisThread.getName();
        int rc = 0;
        //final Context old = sshScope.set(context);
        try {
          //context.started = TimeUtil.nowMs();
          thisThread.setName("SSH " + taskName);
          thunk.run();
          out.flush();
@@ -231,6 +234,11 @@
  }
  /** Runnable function which can retrieve a project name related to the task */
  public static interface RepositoryCommandRunnable extends CommandRunnable {
    public String getRepository();
  }
  /**
   * Spawn a function into its own thread.
   * <p>
src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
@@ -27,7 +27,12 @@
import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import com.gitblit.git.GitblitReceivePackFactory;
import com.gitblit.git.GitblitUploadPackFactory;
import com.gitblit.git.RepositoryResolver;
import com.gitblit.transport.ssh.AbstractGitCommand;
import com.gitblit.transport.ssh.CommandMetaData;
import com.gitblit.transport.ssh.SshSession;
import com.gitblit.utils.cli.SubcommandHandler;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
@@ -95,11 +100,11 @@
          bc.setName(getName() + " " + commandName);
        }
        bc.setArguments(args.toArray(new String[args.size()]));
      } else if (!args.isEmpty()) {
        throw new UnloggedFailure(1, commandName + " does not take arguments");
      }
      provideStateTo(cmd);
      provideBaseStateTo(cmd);
      provideGitState(cmd);
      reset();
      //atomicCmd.set(cmd);
      cmd.start(env);
@@ -136,7 +141,7 @@
  }
  @Override
protected String usage() {
  protected String usage() {
    final StringBuilder usage = new StringBuilder();
    usage.append("Available commands");
    if (!getName().isEmpty()) {
@@ -173,4 +178,39 @@
    usage.append("\n");
    return usage.toString();
  }
  // This is needed because we are not using provider or
  // clazz.newInstance() for DispatchCommand
  private void reset() {
      args = new ArrayList<String>();
  }
  private void provideGitState(Command cmd) {
      if (cmd instanceof AbstractGitCommand) {
        AbstractGitCommand a = (AbstractGitCommand) cmd;
        a.setRepositoryResolver(repositoryResolver);
        a.setUploadPackFactory(gitblitUploadPackFactory);
        a.setReceivePackFactory(gitblitReceivePackFactory);
      } else if (cmd instanceof DispatchCommand) {
        DispatchCommand d = (DispatchCommand)cmd;
        d.setRepositoryResolver(repositoryResolver);
        d.setUploadPackFactory(gitblitUploadPackFactory);
        d.setReceivePackFactory(gitblitReceivePackFactory);
      }
  }
  private RepositoryResolver<SshSession> repositoryResolver;
  public void setRepositoryResolver(RepositoryResolver<SshSession> repositoryResolver) {
      this.repositoryResolver = repositoryResolver;
  }
  private GitblitUploadPackFactory<SshSession> gitblitUploadPackFactory;
  public void setUploadPackFactory(GitblitUploadPackFactory<SshSession> gitblitUploadPackFactory) {
      this.gitblitUploadPackFactory = gitblitUploadPackFactory;
  }
  private GitblitReceivePackFactory<SshSession> gitblitReceivePackFactory;
  public void setReceivePackFactory(GitblitReceivePackFactory<SshSession> gitblitReceivePackFactory) {
      this.gitblitReceivePackFactory = gitblitReceivePackFactory;
  }
}
src/main/java/com/gitblit/transport/ssh/commands/Receive.java
New file
@@ -0,0 +1,34 @@
/*
 * Copyright 2014 gitblit.com.
 *
 * Licensed 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 com.gitblit.transport.ssh.commands;
import org.eclipse.jgit.transport.ReceivePack;
import com.gitblit.transport.ssh.AbstractGitCommand;
import com.gitblit.transport.ssh.CommandMetaData;
@CommandMetaData(name = "git-receive-pack", description = "Receive pack")
public class Receive extends AbstractGitCommand {
    @Override
    protected void runImpl() throws Failure {
        try {
            ReceivePack rp = receivePackFactory.create(ctx.getSession(), repo);
            rp.receive(in, out, null);
        } catch (Exception e) {
            throw new Failure(1, "fatal: Cannot receive pack: ", e);
        }
    }
}
src/main/java/com/gitblit/transport/ssh/commands/Upload.java
New file
@@ -0,0 +1,39 @@
/*
 * Copyright 2014 gitblit.com.
 *
 * Licensed 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 com.gitblit.transport.ssh.commands;
import javax.inject.Inject;
import org.eclipse.jgit.transport.UploadPack;
import org.eclipse.jgit.transport.resolver.UploadPackFactory;
import com.gitblit.git.RepositoryResolver;
import com.gitblit.transport.ssh.AbstractGitCommand;
import com.gitblit.transport.ssh.CommandMetaData;
import com.gitblit.transport.ssh.SshSession;
@CommandMetaData(name = "git-upload-pack", description = "Upload pack")
public class Upload extends AbstractGitCommand {
    @Override
    protected void runImpl() throws Failure {
        try {
            UploadPack up = uploadPackFactory.create(ctx.getSession(), repo);
            up.upload(in, out, null);
        } catch (Exception e) {
            throw new Failure(1, "fatal: Cannot upload pack: ", e);
        }
    }
}
src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java
@@ -29,7 +29,7 @@
  @Override
  public void run() {
    stdout.println(String.format("Version: %s", Constants.getGitBlitVersion(),
      stdout.println(String.format("Version: %s", Constants.getGitBlitVersion(),
        verbose));
  }
}