Create git and gitblit dispatchers
diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
index b8dd5b9..de7aad1 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java
@@ -34,21 +34,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.gitblit.git.GitblitReceivePackFactory;
-import com.gitblit.git.GitblitUploadPackFactory;
-import com.gitblit.git.RepositoryResolver;
 import com.gitblit.manager.IGitblit;
 import com.gitblit.models.UserModel;
-import com.gitblit.transport.ssh.commands.AddKeyCommand;
-import com.gitblit.transport.ssh.commands.CreateRepository;
 import com.gitblit.transport.ssh.commands.DispatchCommand;
-import com.gitblit.transport.ssh.commands.ListRepositoriesCommand;
-import com.gitblit.transport.ssh.commands.Receive;
-import com.gitblit.transport.ssh.commands.RemoveKeyCommand;
-import com.gitblit.transport.ssh.commands.ReviewCommand;
-import com.gitblit.transport.ssh.commands.SetAccountCommand;
-import com.gitblit.transport.ssh.commands.Upload;
-import com.gitblit.transport.ssh.commands.VersionCommand;
+import com.gitblit.transport.ssh.git.GitDispatchCommand;
+import com.gitblit.transport.ssh.gitblit.GitblitDispatchCommand;
 import com.gitblit.utils.IdGenerator;
 import com.gitblit.utils.WorkQueue;
 import com.google.common.util.concurrent.Atomics;
@@ -86,31 +76,16 @@
 	protected DispatchCommand createRootDispatcher(SshDaemonClient client, String cmdLine) {
 		final UserModel user = client.getUser();
 
-		DispatchCommand gitblitCmd = new DispatchCommand();
-		gitblitCmd.registerCommand(user, VersionCommand.class);
-		gitblitCmd.registerCommand(user, AddKeyCommand.class);
-		gitblitCmd.registerCommand(user, RemoveKeyCommand.class);
-		gitblitCmd.registerCommand(user, ListRepositoriesCommand.class);
-		gitblitCmd.registerCommand(user, ReviewCommand.class);
-
-		gitblitCmd.registerCommand(user, CreateRepository.class);
-		gitblitCmd.registerCommand(user, SetAccountCommand.class);
-
-		DispatchCommand gitCmd = new DispatchCommand();
-		gitCmd.registerCommand(user, Upload.class);
-		gitCmd.registerCommand(user, Receive.class);
-
-		DispatchCommand root = new DispatchCommand();
-		root.registerDispatcher("gitblit", gitblitCmd);
-		root.registerDispatcher("git", gitCmd);
-
-		root.setRepositoryResolver(new RepositoryResolver<SshDaemonClient>(gitblit));
-		root.setUploadPackFactory(new GitblitUploadPackFactory<SshDaemonClient>(gitblit));
-		root.setReceivePackFactory(new GitblitReceivePackFactory<SshDaemonClient>(gitblit));
-		root.setAuthenticator(keyAuthenticator);
-
+		DispatchCommand root = new DispatchCommand() {
+		};
 		root.setContext(new SshCommandContext(gitblit, client, cmdLine));
 
+		// TODO convert these dispatchers to plugin extension points
+		root.registerDispatcher(user, GitblitDispatchCommand.class);
+		root.registerDispatcher(user, GitDispatchCommand.class);
+
+		root.setAuthenticator(keyAuthenticator);
+
 		return root;
 	}
 
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 00d3b81..38f1a48 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
@@ -30,21 +30,17 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.gitblit.git.GitblitReceivePackFactory;
-import com.gitblit.git.GitblitUploadPackFactory;
-import com.gitblit.git.RepositoryResolver;
 import com.gitblit.models.UserModel;
 import com.gitblit.transport.ssh.CommandMetaData;
 import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator;
-import com.gitblit.transport.ssh.SshDaemonClient;
-mport com.gitblit.utils.StringUtils;
+import com.gitblit.transport.ssh.gitblit.BaseKeyCommand;
+import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.cli.SubcommandHandler;
 import com.google.common.base.Charsets;
 import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
 
-public class DispatchCommand extends BaseCommand {
+public abstract class DispatchCommand extends BaseCommand {
 
 	private Logger log = LoggerFactory.getLogger(getClass());
 
@@ -62,11 +58,31 @@
 		commands = new HashSet<Class<? extends BaseCommand>>();
 	}
 
-	public void registerDispatcher(String name, Command cmd) {
+	public void registerDispatcher(UserModel user, Class<? extends DispatchCommand> cmd) {
+		if (!cmd.isAnnotationPresent(CommandMetaData.class)) {
+			throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(),
+					CommandMetaData.class.getName()));
+		}
 		if (dispatchers == null) {
 			dispatchers = Maps.newHashMap();
 		}
-		dispatchers.put(name, cmd);
+
+		CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class);
+		if (meta.admin() && !user.canAdmin()) {
+			log.debug(MessageFormat.format("excluding admin dispatch command {0} for {1}", meta.name(), user.username));
+			return;
+		}
+
+		try {
+			DispatchCommand dispatcher = cmd.newInstance();
+			dispatcher.registerCommands(user);
+			dispatchers.put(meta.name(), dispatcher);
+		} catch (Exception e) {
+			log.error("failed to register {} dispatcher", meta.name());
+		}
+	}
+
+	protected void registerCommands(UserModel user) {
 	}
 
 
@@ -237,41 +253,12 @@
 		cmd.setErrorStream(err);
 		cmd.setExitCallback(exit);
 
-		if (cmd instanceof BaseGitCommand) {
-			BaseGitCommand a = (BaseGitCommand) 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);
-			d.setAuthenticator(authenticator);
-		} else if (cmd instanceof BaseKeyCommand) {
+		if (cmd instanceof BaseKeyCommand) {
 			BaseKeyCommand k = (BaseKeyCommand) cmd;
 			k.setAuthenticator(authenticator);
 		}
 	}
 
-	private RepositoryResolver<SshDaemonClient> repositoryResolver;
-
-	public void setRepositoryResolver(RepositoryResolver<SshDaemonClient> repositoryResolver) {
-		this.repositoryResolver = repositoryResolver;
-	}
-
-	private GitblitUploadPackFactory<SshDaemonClient> gitblitUploadPackFactory;
-
-	public void setUploadPackFactory(GitblitUploadPackFactory<SshDaemonClient> gitblitUploadPackFactory) {
-		this.gitblitUploadPackFactory = gitblitUploadPackFactory;
-	}
-
-	private GitblitReceivePackFactory<SshDaemonClient> gitblitReceivePackFactory;
-
-	public void setReceivePackFactory(GitblitReceivePackFactory<SshDaemonClient> gitblitReceivePackFactory) {
-		this.gitblitReceivePackFactory = gitblitReceivePackFactory;
-	}
-
 	private CachingPublicKeyAuthenticator authenticator;
 
 	public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) {
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java
deleted file mode 100644
index 7c58e7f..0000000
--- a/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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 java.text.MessageFormat;
-import java.text.SimpleDateFormat;
-import java.util.List;
-
-import org.kohsuke.args4j.Option;
-import org.parboiled.common.StringUtils;
-
-import com.gitblit.manager.IGitblit;
-import com.gitblit.models.RepositoryModel;
-import com.gitblit.models.UserModel;
-import com.gitblit.transport.ssh.CommandMetaData;
-
-@CommandMetaData(name = "repositories", description = "List the available repositories")
-public class ListRepositoriesCommand extends SshCommand {
-
-	@Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
-	private boolean verbose;
-
-	@Override
-	public void run() {
-		IGitblit gitblit = ctx.getGitblit();
-		UserModel user = ctx.getClient().getUser();
-		List<RepositoryModel> repositories = gitblit.getRepositoryModels(user);
-		int nameLen = 0;
-		int descLen = 0;
-		for (RepositoryModel repo : repositories) {
-			int len = repo.name.length();
-			if (len > nameLen) {
-				nameLen = len;
-			}
-			if (!StringUtils.isEmpty(repo.description)) {
-				len = repo.description.length();
-				if (len > descLen) {
-					descLen = len;
-				}
-			}
-		}
-		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
-
-		String pattern;
-		if (verbose) {
-			pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%s", nameLen, descLen);
-		} else {
-			pattern = "%s";
-		}
-
-		for (RepositoryModel repo : repositories) {
-			stdout.println(String.format(pattern,
-					repo.name,
-					repo.description == null ? "" : repo.description,
-					df.format(repo.lastChange)));
-		}
-	}
-}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java b/src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java
similarity index 95%
rename from src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java
rename to src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java
index a341161..b203d47 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.git;
 
 import java.io.IOException;
 
@@ -27,12 +27,13 @@
 import com.gitblit.git.GitblitUploadPackFactory;
 import com.gitblit.git.RepositoryResolver;
 import com.gitblit.transport.ssh.SshDaemonClient;
+import com.gitblit.transport.ssh.commands.BaseCommand;
 
 /**
  * @author Eric Myhre
  *
  */
-public abstract class BaseGitCommand extends BaseCommand {
+abstract class BaseGitCommand extends BaseCommand {
 	@Argument(index = 0, metaVar = "REPOSITORY", required = true, usage = "repository name")
 	protected String repository;
 
diff --git a/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java
new file mode 100644
index 0000000..adeace5
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java
@@ -0,0 +1,61 @@
+/*
+ * 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.git;
+
+import com.gitblit.git.GitblitReceivePackFactory;
+import com.gitblit.git.GitblitUploadPackFactory;
+import com.gitblit.git.RepositoryResolver;
+import com.gitblit.manager.IGitblit;
+import com.gitblit.models.UserModel;
+import com.gitblit.transport.ssh.CommandMetaData;
+import com.gitblit.transport.ssh.SshCommandContext;
+import com.gitblit.transport.ssh.SshDaemonClient;
+import com.gitblit.transport.ssh.commands.BaseCommand;
+import com.gitblit.transport.ssh.commands.DispatchCommand;
+
+@CommandMetaData(name = "git", description="Dispatcher for git receive and upload commands", hidden = true)
+public class GitDispatchCommand extends DispatchCommand {
+
+	protected RepositoryResolver<SshDaemonClient> repositoryResolver;
+	protected GitblitUploadPackFactory<SshDaemonClient> uploadPackFactory;
+	protected GitblitReceivePackFactory<SshDaemonClient> receivePackFactory;
+
+	@Override
+	public void setContext(SshCommandContext context) {
+		super.setContext(context);
+
+		IGitblit gitblit = context.getGitblit();
+		repositoryResolver = new RepositoryResolver<SshDaemonClient>(gitblit);
+		uploadPackFactory = new GitblitUploadPackFactory<SshDaemonClient>(gitblit);
+		receivePackFactory = new GitblitReceivePackFactory<SshDaemonClient>(gitblit);
+	}
+
+	@Override
+	protected void registerCommands(UserModel user) {
+		registerCommand(user, Upload.class);
+		registerCommand(user, Receive.class);
+	}
+
+	@Override
+	protected void provideStateTo(final BaseCommand cmd) {
+		super.provideStateTo(cmd);
+
+		BaseGitCommand a = (BaseGitCommand) cmd;
+		a.setRepositoryResolver(repositoryResolver);
+		a.setUploadPackFactory(uploadPackFactory);
+		a.setReceivePackFactory(receivePackFactory);
+	}
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java b/src/main/java/com/gitblit/transport/ssh/git/Receive.java
similarity index 93%
rename from src/main/java/com/gitblit/transport/ssh/commands/Receive.java
rename to src/main/java/com/gitblit/transport/ssh/git/Receive.java
index 559cfa5..4089f1d 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java
+++ b/src/main/java/com/gitblit/transport/ssh/git/Receive.java
@@ -13,13 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.git;
 
 import org.eclipse.jgit.transport.ReceivePack;
 
 import com.gitblit.transport.ssh.CommandMetaData;
 
-@CommandMetaData(name = "git-receive-pack", description = "Receive pack")
+@CommandMetaData(name = "git-receive-pack", description = "Receives pushes from a client")
 public class Receive extends BaseGitCommand {
 	@Override
 	protected void runImpl() throws Failure {
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java b/src/main/java/com/gitblit/transport/ssh/git/Upload.java
similarity index 87%
rename from src/main/java/com/gitblit/transport/ssh/commands/Upload.java
rename to src/main/java/com/gitblit/transport/ssh/git/Upload.java
index ac98bb2..5793c3e 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java
+++ b/src/main/java/com/gitblit/transport/ssh/git/Upload.java
@@ -13,13 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.git;
 
 import org.eclipse.jgit.transport.UploadPack;
 
 import com.gitblit.transport.ssh.CommandMetaData;
 
-@CommandMetaData(name = "git-upload-pack", description = "Upload pack")
+@CommandMetaData(name = "git-upload-pack", description = "Sends packs to a client for clone and fetch")
 public class Upload extends BaseGitCommand {
 	@Override
 	protected void runImpl() throws Failure {
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java
similarity index 88%
rename from src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java
rename to src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java
index 35bb1bb..6d5c85c 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java
@@ -13,7 +13,7 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.gitblit;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -24,7 +24,6 @@
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.transport.ssh.CommandMetaData;
-import com.gitblit.transport.ssh.IKeyManager;
 
 /**
  * Add a key to the current user's authorized keys list.
@@ -44,9 +43,8 @@
 	public void run() throws IOException, UnloggedFailure {
 		String username = ctx.getClient().getUsername();
 		List<String> keys = readKeys(addKeys);
-		IKeyManager keyManager = authenticator.getKeyManager();
 		for (String key : keys) {
-			keyManager.addKey(username, key);
+			getKeyManager().addKey(username, key);
 			log.info("added SSH public key for {}", username);
 		}
 	}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
similarity index 85%
rename from src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java
rename to src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
index f92ea6f..0909957 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
@@ -13,7 +13,7 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.gitblit;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -21,7 +21,9 @@
 import java.io.UnsupportedEncodingException;
 import java.util.List;
 
+import com.gitblit.transport.ssh.IKeyManager;
 import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator;
+import com.gitblit.transport.ssh.commands.SshCommand;
 import com.google.common.base.Charsets;
 
 /**
@@ -29,7 +31,7 @@
  * Base class for commands that read SSH keys from stdin or a parameter list.
  *
  */
-public abstract class BaseKeyCommand extends SshCommand {
+abstract class BaseKeyCommand extends SshCommand {
 
 	protected List<String> readKeys(List<String> sshKeys)
 			throws UnsupportedEncodingException, IOException {
@@ -55,4 +57,8 @@
 	public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) {
 		this.authenticator = authenticator;
 	}
+	
+	protected IKeyManager getKeyManager() {
+		return authenticator.getKeyManager();
+	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java
similarity index 92%
rename from src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java
rename to src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java
index 20f6901..b2e1b1b 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.gitblit;
 
 import org.kohsuke.args4j.Option;
 
 import com.gitblit.transport.ssh.CommandMetaData;
+import com.gitblit.transport.ssh.commands.SshCommand;
 
 @CommandMetaData(name = "create-repository", description = "Create new GIT repository", admin = true, hidden = true)
 public class CreateRepository extends SshCommand {
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java
new file mode 100644
index 0000000..544b204
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java
@@ -0,0 +1,38 @@
+/*
+ * 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.gitblit;
+
+import com.gitblit.models.UserModel;
+import com.gitblit.transport.ssh.CommandMetaData;
+import com.gitblit.transport.ssh.commands.DispatchCommand;
+
+@CommandMetaData(name = "gitblit", description = "Gitblit server commands")
+public class GitblitDispatchCommand extends DispatchCommand {
+
+	@Override
+	protected void registerCommands(UserModel user) {
+		// normal usage commands
+		registerCommand(user, VersionCommand.class);
+		registerCommand(user, AddKeyCommand.class);
+		registerCommand(user, RemoveKeyCommand.class);
+		registerCommand(user, LsCommand.class);
+		registerCommand(user, ReviewCommand.class);
+
+		// administrative commands
+		registerCommand(user, CreateRepository.class);
+		registerCommand(user, SetAccountCommand.class);
+	}
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java
new file mode 100644
index 0000000..cf50a2e
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java
@@ -0,0 +1,122 @@
+/*
+ * 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.gitblit;
+
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+import org.kohsuke.args4j.Option;
+import org.parboiled.common.StringUtils;
+
+import com.gitblit.manager.IGitblit;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.transport.ssh.CommandMetaData;
+import com.gitblit.transport.ssh.commands.SshCommand;
+
+@CommandMetaData(name = "ls", description = "List repositories or projects")
+public class LsCommand extends SshCommand {
+
+	@Option(name = "--projects", aliases = { "-p" }, usage = "list projects")
+	private boolean projects;
+
+	@Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
+	private boolean verbose;
+
+	@Override
+	public void run() {
+		if (projects) {
+			listProjects();
+		} else {
+			listRepositories();
+		}
+	}
+
+	protected void listProjects() {
+		IGitblit gitblit = ctx.getGitblit();
+		UserModel user = ctx.getClient().getUser();
+		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+
+		List<ProjectModel> projects = gitblit.getProjectModels(user, false);
+		int nameLen = 0;
+		int descLen = 0;
+		for (ProjectModel project : projects) {
+			int len = project.name.length();
+			if (len > nameLen) {
+				nameLen = len;
+			}
+			if (!StringUtils.isEmpty(project.description)) {
+				len = project.description.length();
+				if (len > descLen) {
+					descLen = len;
+				}
+			}
+		}
+
+		String pattern;
+		if (verbose) {
+			pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%s", nameLen, descLen);
+		} else {
+			pattern = "%s";
+		}
+
+		for (ProjectModel project : projects) {
+			stdout.println(String.format(pattern,
+					project.name,
+					project.description == null ? "" : project.description,
+					df.format(project.lastChange)));
+		}
+	}
+
+	protected void listRepositories() {
+		IGitblit gitblit = ctx.getGitblit();
+		UserModel user = ctx.getClient().getUser();
+		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+
+		List<RepositoryModel> repositories = gitblit.getRepositoryModels(user);
+		int nameLen = 0;
+		int descLen = 0;
+		for (RepositoryModel repo : repositories) {
+			int len = repo.name.length();
+			if (len > nameLen) {
+				nameLen = len;
+			}
+			if (!StringUtils.isEmpty(repo.description)) {
+				len = repo.description.length();
+				if (len > descLen) {
+					descLen = len;
+				}
+			}
+		}
+
+		String pattern;
+		if (verbose) {
+			pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%s", nameLen, descLen);
+		} else {
+			pattern = "%s";
+		}
+
+		for (RepositoryModel repo : repositories) {
+			stdout.println(String.format(pattern,
+					repo.name,
+					repo.description == null ? "" : repo.description,
+					df.format(repo.lastChange)));
+		}
+	}
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java
similarity index 87%
rename from src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java
rename to src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java
index 90e7041..7c9abfd 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java
@@ -13,7 +13,7 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.gitblit;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -24,7 +24,6 @@
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.transport.ssh.CommandMetaData;
-import com.gitblit.transport.ssh.IKeyManager;
 
 
 /**
@@ -47,13 +46,12 @@
 	public void run() throws IOException, UnloggedFailure {
 		String username = ctx.getClient().getUsername();
 		List<String> keys = readKeys(removeKeys);
-		IKeyManager keyManager = authenticator.getKeyManager();
 		if (keys.contains(ALL)) {
-			keyManager.removeAllKeys(username);
+			getKeyManager().removeAllKeys(username);
 			log.info("removed all SSH public keys from {}", username);
 		} else {
 			for (String key : keys) {
-				keyManager.removeKey(username, key);
+				getKeyManager().removeKey(username, key);
 				log.info("removed SSH public key from {}", username);
 			}
 		}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java
similarity index 96%
rename from src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java
rename to src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java
index b088a2e..9e4d8ba 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.gitblit;
 
 import java.util.HashSet;
 import java.util.Set;
@@ -26,6 +26,7 @@
 import com.gitblit.models.TicketModel.Score;
 import com.gitblit.models.UserModel;
 import com.gitblit.transport.ssh.CommandMetaData;
+import com.gitblit.transport.ssh.commands.SshCommand;
 import com.gitblit.wicket.GitBlitWebSession;
 
 @CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets", hidden = true)
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java
similarity index 88%
rename from src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java
rename to src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java
index 1f0d902..28ac9e1 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java
@@ -12,7 +12,7 @@
 //See the License for the specific language governing permissions and
 //limitations under the License.
 
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.gitblit;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -23,7 +23,6 @@
 import org.kohsuke.args4j.Option;
 
 import com.gitblit.transport.ssh.CommandMetaData;
-import com.gitblit.transport.ssh.IKeyManager;
 
 /** Set a user's account settings. **/
 @CommandMetaData(name = "set-account", description = "Change an account's settings", admin = true)
@@ -69,19 +68,17 @@
 
 	private void addSshKeys(List<String> sshKeys) throws UnloggedFailure,
 			IOException {
-		IKeyManager keyManager = authenticator.getKeyManager();
 		for (String sshKey : sshKeys) {
-			keyManager.addKey(user, sshKey);
+			getKeyManager().addKey(user, sshKey);
 		}
 	}
 
 	private void deleteSshKeys(List<String> sshKeys) {
-		IKeyManager keyManager = authenticator.getKeyManager();
 		if (sshKeys.contains(ALL)) {
-			keyManager.removeAllKeys(user);
+			getKeyManager().removeAllKeys(user);
 		} else {
 			for (String sshKey : sshKeys) {
-				keyManager.removeKey(user, sshKey);
+				getKeyManager().removeKey(user, sshKey);
 			}
 		}
 	}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java
similarity index 90%
rename from src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java
rename to src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java
index c2c4f52..513f6d9 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.gitblit.transport.ssh.commands;
+package com.gitblit.transport.ssh.gitblit;
 
 import com.gitblit.Constants;
 import com.gitblit.transport.ssh.CommandMetaData;
+import com.gitblit.transport.ssh.commands.SshCommand;
 
 @CommandMetaData(name="version", description = "Display the Gitblit version")
 public class VersionCommand extends SshCommand {