From e3b636e7fa2a823cfe90ea75e88034a60f7e59e6 Mon Sep 17 00:00:00 2001
From: David Ostrovsky <david@ostrovsky.org>
Date: Thu, 10 Apr 2014 18:58:07 -0400
Subject: [PATCH] SSHD: Add support for git pack commands

---
 src/main/java/com/gitblit/transport/ssh/SshSession.java               |    9 
 src/main/java/com/gitblit/manager/IAuthenticationManager.java         |    3 
 src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java       |  108 ++++++++++
 src/main/java/com/gitblit/Constants.java                              |    2 
 src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java     |   38 ++-
 src/main/java/com/gitblit/git/RepositoryResolver.java                 |   10 +
 src/main/java/com/gitblit/transport/ssh/commands/Receive.java         |   34 +++
 src/main/java/com/gitblit/git/GitblitReceivePackFactory.java          |    8 
 src/main/java/com/gitblit/transport/ssh/SshContext.java               |   35 +++
 src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java  |    2 
 src/main/java/com/gitblit/transport/ssh/SshDaemon.java                |   24 +-
 src/main/java/com/gitblit/transport/ssh/commands/Upload.java          |   39 +++
 src/main/java/com/gitblit/manager/GitblitManager.java                 |    7 
 src/main/java/com/gitblit/manager/ServicesManager.java                |    1 
 src/main/java/com/gitblit/manager/AuthenticationManager.java          |   29 ++
 src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java |   48 ++++
 src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java        |  147 +-------------
 17 files changed, 379 insertions(+), 165 deletions(-)

diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 2a98b53..889e5a3 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/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();
diff --git a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
index 7976fe5..9911258 100644
--- a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
+++ b/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);
diff --git a/src/main/java/com/gitblit/git/RepositoryResolver.java b/src/main/java/com/gitblit/git/RepositoryResolver.java
index 208c1ae..c859f6f 100644
--- a/src/main/java/com/gitblit/git/RepositoryResolver.java
+++ b/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)) {
diff --git a/src/main/java/com/gitblit/manager/AuthenticationManager.java b/src/main/java/com/gitblit/manager/AuthenticationManager.java
index 4f3e652..47425ce 100644
--- a/src/main/java/com/gitblit/manager/AuthenticationManager.java
+++ b/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.
diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java
index b6c2b47..a5a2637 100644
--- a/src/main/java/com/gitblit/manager/GitblitManager.java
+++ b/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);
diff --git a/src/main/java/com/gitblit/manager/IAuthenticationManager.java b/src/main/java/com/gitblit/manager/IAuthenticationManager.java
index 3007a30..5d98d12 100644
--- a/src/main/java/com/gitblit/manager/IAuthenticationManager.java
+++ b/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.
 	 *
diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java
index df8918e..11083be 100644
--- a/src/main/java/com/gitblit/manager/ServicesManager.java
+++ b/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));
 		}
-
 	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java b/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java
new file mode 100644
index 0000000..bba6a40
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java
@@ -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;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
index 056938e..0c8492f 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
+++ b/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);
-		}
-	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/SshContext.java b/src/main/java/com/gitblit/transport/ssh/SshContext.java
new file mode 100644
index 0000000..b137cb8
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/SshContext.java
@@ -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;
+	}
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
index dd4a2d8..b23ddd5 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
+++ b/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);
 
diff --git a/src/main/java/com/gitblit/transport/ssh/SshSession.java b/src/main/java/com/gitblit/transport/ssh/SshSession.java
index 9f18a19..ffff8af 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshSession.java
+++ b/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;
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
index fd73ccf..a04c505 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
+++ b/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>
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
index b6944ea..597b9ea 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
+++ b/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;
+  }
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java
new file mode 100644
index 0000000..dd1e8a0
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java
@@ -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);
+		}
+	}
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java
new file mode 100644
index 0000000..d6c3f96
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java
@@ -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);
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java
index baae6a2..fc3e01b 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java
+++ b/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));
   }
 }

--
Gitblit v1.9.1