From 5bed299423d4ba418c64732c51e567817a7f7e45 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 10 Apr 2014 18:58:09 -0400
Subject: [PATCH] Revise dispatcher setup and command registration

---
 src/main/java/com/gitblit/transport/ssh/gitblit/TicketsDispatcher.java      |    4 
 src/main/java/com/gitblit/transport/ssh/gitblit/ListDispatcher.java         |  148 --------------
 src/main/java/com/gitblit/transport/ssh/gitblit/ProjectsDispatcher.java     |   84 ++++++++
 src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java        |   18 -
 src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java      |   19 +
 src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java |   84 ++++++++
 src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java       |   74 +++++--
 src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java         |    8 
 src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java        |  106 ++++++++++
 src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java              |   10 
 10 files changed, 361 insertions(+), 194 deletions(-)

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 b5d131f..b916bb1 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
@@ -84,16 +84,54 @@
 		dispatchers.clear();
 	}
 
-	protected void registerDispatcher(UserModel user, Class<? extends DispatchCommand> cmd) {
+	/**
+	 * Setup this dispatcher. Commands and nested dispatchers are normally
+	 * registered within this method.
+	 *
+	 * @param user
+	 */
+	protected abstract void setup(UserModel user);
+
+	/**
+	 * Register a command or a dispatcher by it's class.
+	 *
+	 * @param user
+	 * @param clazz
+	 */
+	@SuppressWarnings("unchecked")
+	protected final void register(UserModel user, Class<? extends BaseCommand> clazz) {
+		if (DispatchCommand.class.isAssignableFrom(clazz)) {
+			registerDispatcher(user, (Class<? extends DispatchCommand>) clazz);
+			return;
+		}
+
+		registerCommand(user, clazz);
+	}
+
+	/**
+	 * Register a command or a dispatcher instance.
+	 *
+	 * @param user
+	 * @param cmd
+	 */
+	protected final void register(UserModel user, BaseCommand cmd) {
+		if (cmd instanceof DispatchCommand) {
+			registerDispatcher(user, (DispatchCommand) cmd);
+			return;
+		}
+		registerCommand(user, cmd);
+	}
+
+	private void registerDispatcher(UserModel user, Class<? extends DispatchCommand> clazz) {
 		try {
-			DispatchCommand dispatcher = cmd.newInstance();
+			DispatchCommand dispatcher = clazz.newInstance();
 			registerDispatcher(user, dispatcher);
 		} catch (Exception e) {
-			log.error("failed to instantiate {}", cmd.getName());
+			log.error("failed to instantiate {}", clazz.getName());
 		}
 	}
 
-	protected void registerDispatcher(UserModel user, DispatchCommand dispatcher) {
+	private void registerDispatcher(UserModel user, DispatchCommand dispatcher) {
 		Class<? extends DispatchCommand> dispatcherClass = dispatcher.getClass();
 		if (!dispatcherClass.isAnnotationPresent(CommandMetaData.class)) {
 			throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", dispatcher.getName(),
@@ -108,7 +146,7 @@
 		}
 
 		try {
-			dispatcher.registerCommands(user);
+			dispatcher.setup(user);
 			if (dispatcher.commands.isEmpty() && dispatcher.dispatchers.isEmpty()) {
 				log.debug(MessageFormat.format("excluding empty dispatcher {0} for {1}",
 						meta.name(), user.username));
@@ -129,30 +167,23 @@
 		}
 	}
 
-	protected abstract void registerCommands(UserModel user);
-
 	/**
 	 * Registers a command as long as the user is permitted to execute it.
 	 *
 	 * @param user
-	 * @param cmd
+	 * @param clazz
 	 */
-	protected void registerCommand(UserModel user, Class<? extends BaseCommand> cmd) {
-		if (DispatchCommand.class.isAssignableFrom(cmd)) {
-			log.warn("{} tried to register {} with registerCommand", getName(), cmd.getName());
-			registerDispatcher(user, (Class<? extends DispatchCommand>) cmd);
-			return;
-		}
-		if (!cmd.isAnnotationPresent(CommandMetaData.class)) {
-			throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(),
+	private void registerCommand(UserModel user, Class<? extends BaseCommand> clazz) {
+		if (!clazz.isAnnotationPresent(CommandMetaData.class)) {
+			throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", clazz.getName(),
 					CommandMetaData.class.getName()));
 		}
-		CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class);
+		CommandMetaData meta = clazz.getAnnotation(CommandMetaData.class);
 		if (meta.admin() && !user.canAdmin()) {
 			log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username));
 			return;
 		}
-		commands.add(cmd);
+		commands.add(clazz);
 	}
 
 	/**
@@ -161,12 +192,7 @@
 	 * @param user
 	 * @param cmd
 	 */
-	protected void registerCommand(UserModel user, BaseCommand cmd) {
-		if (DispatchCommand.class.isAssignableFrom(cmd.getClass())) {
-			log.warn("{} tried to register {} dispatcher with registerCommand", getName(), cmd.getName());
-			registerDispatcher(user, (DispatchCommand) cmd);
-			return;
-		}
+	private void registerCommand(UserModel user, BaseCommand cmd) {
 		if (!cmd.getClass().isAnnotationPresent(CommandMetaData.class)) {
 			throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(),
 					CommandMetaData.class.getName()));
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java
index 896391f..8a871eb 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java
@@ -31,7 +31,8 @@
  * other commands.
  *
  */
-public class RootDispatcher extends DispatchCommand {
+@CommandMetaData(name = "")
+class RootDispatcher extends DispatchCommand {
 
 	private Logger log = LoggerFactory.getLogger(getClass());
 
@@ -39,9 +40,9 @@
 		super();
 		setContext(new SshCommandContext(gitblit, client, cmdLine));
 
-		final UserModel user = client.getUser();
-		registerDispatcher(user, GitblitDispatcher.class);
-		registerDispatcher(user, GitDispatcher.class);
+		UserModel user = client.getUser();
+		register(user, GitblitDispatcher.class);
+		register(user, GitDispatcher.class);
 
 		List<DispatchCommand> exts = gitblit.getExtensions(DispatchCommand.class);
 		for (DispatchCommand ext : exts) {
@@ -49,16 +50,11 @@
 			String plugin = gitblit.whichPlugin(extClass).getDescriptor().getPluginId();
 			CommandMetaData meta = extClass.getAnnotation(CommandMetaData.class);
 			log.info("Dispatcher {} is loaded from plugin {}", meta.name(), plugin);
-			registerDispatcher(user, ext);
+			register(user, ext);
 		}
 	}
 
 	@Override
-	protected final void registerCommands(UserModel user) {
-	}
-
-	@Override
-	protected final void registerCommand(UserModel user, Class<? extends BaseCommand> cmd) {
-		throw new RuntimeException("The root dispatcher does not accept commands, only dispatchers!");
+	protected final void setup(UserModel user) {
 	}
 }
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java b/src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java
index 69dcd81..64f9c8d 100644
--- a/src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java
@@ -26,7 +26,7 @@
 import com.gitblit.transport.ssh.commands.DispatchCommand;
 import com.gitblit.transport.ssh.commands.SshCommandContext;
 
-@CommandMetaData(name = "git", description="Git commands")
+@CommandMetaData(name = "git", description="Git repository commands")
 public class GitDispatcher extends DispatchCommand {
 
 	protected RepositoryResolver<SshDaemonClient> repositoryResolver;
@@ -53,10 +53,10 @@
 	}
 
 	@Override
-	protected void registerCommands(UserModel user) {
-		registerCommand(user, Upload.class);
-		registerCommand(user, Receive.class);
-		registerCommand(user, GarbageCollectionCommand.class);
+	protected void setup(UserModel user) {
+		register(user, Upload.class);
+		register(user, Receive.class);
+		register(user, GarbageCollectionCommand.class);
 	}
 
 	@Override
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java
index f9b5f4f..86d8a8c 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java
@@ -23,16 +23,19 @@
 public class GitblitDispatcher extends DispatchCommand {
 
 	@Override
-	protected void registerCommands(UserModel user) {
+	protected void setup(UserModel user) {
 		// commands in this dispatcher
-		registerCommand(user, VersionCommand.class);
-		registerCommand(user, CreateRepository.class);
-		registerCommand(user, SetAccountCommand.class);
-		registerCommand(user, ConfigCommand.class);
+		register(user, VersionCommand.class);
+		register(user, CreateRepository.class);
+		register(user, SetAccountCommand.class);
+		register(user, ConfigCommand.class);
 
 		// nested dispatchers
-		registerDispatcher(user, ListDispatcher.class);
-		registerDispatcher(user, KeysDispatcher.class);
-		registerDispatcher(user, TicketsDispatcher.class);
+		register(user, ListDispatcher.class);
+		register(user, KeysDispatcher.class);
+		register(user, TicketsDispatcher.class);
+		register(user, UsersDispatcher.class);
+		register(user, ProjectsDispatcher.class);
+		register(user, RepositoriesDispatcher.class);
 	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
index a54196d..8c1bfd2 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
@@ -42,10 +42,10 @@
 public class KeysDispatcher extends DispatchCommand {
 
 	@Override
-	protected void registerCommands(UserModel user) {
-		registerCommand(user, AddKey.class);
-		registerCommand(user, RemoveKey.class);
-		registerCommand(user, ListKeys.class);
+	protected void setup(UserModel user) {
+		register(user, AddKey.class);
+		register(user, RemoveKey.class);
+		register(user, ListKeys.class);
 	}
 
 	@CommandMetaData(name = "add", description = "Add an SSH public key to your account")
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ListDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ListDispatcher.java
index 0c372df..343e59a 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/ListDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/ListDispatcher.java
@@ -15,20 +15,9 @@
  */
 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.commands.CommandMetaData;
 import com.gitblit.transport.ssh.commands.DispatchCommand;
-import com.gitblit.transport.ssh.commands.SshCommand;
 
 /**
  * The dispatcher and it's commands for Gitblit object listing.
@@ -40,11 +29,11 @@
 public class ListDispatcher extends DispatchCommand {
 
 	@Override
-	protected void registerCommands(UserModel user) {
-		registerCommand(user, ListRepositories.class);
-		registerCommand(user, ListProjects.class);
-		registerCommand(user, ListUsers.class);
-		registerCommand(user, ListKeys.class);
+	protected void setup(UserModel user) {
+		register(user, ListRepositories.class);
+		register(user, ListProjects.class);
+		register(user, ListUsers.class);
+		register(user, ListKeys.class);
 	}
 
 	/* List SSH public keys */
@@ -54,137 +43,16 @@
 
 	/* List repositories */
 	@CommandMetaData(name = "repositories", aliases = { "repos" }, description = "List repositories")
-	public static class ListRepositories extends SshCommand {
-
-		@Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
-		private boolean verbose;
-
-		@Override
-		public void run() {
-			IGitblit gitblit = getContext().getGitblit();
-			UserModel user = getContext().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)));
-			}
-		}
+	public static class ListRepositories extends RepositoriesDispatcher.ListRepositories {
 	}
 
 	/* List projects */
 	@CommandMetaData(name = "projects", description = "List projects")
-	public static class ListProjects extends SshCommand {
-
-		@Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
-		private boolean verbose;
-
-		@Override
-		public void run() {
-			IGitblit gitblit = getContext().getGitblit();
-			UserModel user = getContext().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)));
-			}
-		}
+	public static class ListProjects extends ProjectsDispatcher.ListProjects {
 	}
 
 	/* List users */
 	@CommandMetaData(name = "users", description = "List users", admin = true)
-	public static class ListUsers extends SshCommand {
-
-		@Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
-		private boolean verbose;
-
-		@Override
-		public void run() {
-			IGitblit gitblit = getContext().getGitblit();
-			List<UserModel> users = gitblit.getAllUsers();
-			int displaynameLen = 0;
-			int usernameLen = 0;
-			for (UserModel user : users) {
-				int len = user.getDisplayName().length();
-				if (len > displaynameLen) {
-					displaynameLen = len;
-				}
-				if (!StringUtils.isEmpty(user.username)) {
-					len = user.username.length();
-					if (len > usernameLen) {
-						usernameLen = len;
-					}
-				}
-			}
-
-			String pattern;
-			if (verbose) {
-				pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%-10s\t%s", displaynameLen, usernameLen);
-			} else {
-				pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s", displaynameLen, usernameLen);
-			}
-
-			for (UserModel user : users) {
-				if (user.disabled) {
-					continue;
-				}
-				stdout.println(String.format(pattern,
-						user.getDisplayName(),
-						(user.canAdmin() ? "*":" ") + user.username,
-						user.accountType,
-						user.emailAddress == null ? "" : user.emailAddress));
-			}
-		}
+	public static class ListUsers extends UsersDispatcher.ListUsers {
 	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ProjectsDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ProjectsDispatcher.java
new file mode 100644
index 0000000..cd68754
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/ProjectsDispatcher.java
@@ -0,0 +1,84 @@
+/*
+ * 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.UserModel;
+import com.gitblit.transport.ssh.commands.CommandMetaData;
+import com.gitblit.transport.ssh.commands.DispatchCommand;
+import com.gitblit.transport.ssh.commands.SshCommand;
+
+@CommandMetaData(name = "projects", description = "Project management commands")
+public class ProjectsDispatcher extends DispatchCommand {
+
+	@Override
+	protected void setup(UserModel user) {
+		register(user, ListProjects.class);
+	}
+
+	/* List projects */
+	@CommandMetaData(name = "list", aliases= { "ls" }, description = "List projects")
+	public static class ListProjects extends SshCommand {
+
+		@Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
+		private boolean verbose;
+
+		@Override
+		public void run() {
+			IGitblit gitblit = getContext().getGitblit();
+			UserModel user = getContext().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)));
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
new file mode 100644
index 0000000..3aaed54
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
@@ -0,0 +1,84 @@
+/*
+ * 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.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.transport.ssh.commands.CommandMetaData;
+import com.gitblit.transport.ssh.commands.DispatchCommand;
+import com.gitblit.transport.ssh.commands.SshCommand;
+
+@CommandMetaData(name = "repositories", aliases = { "repos" }, description = "Repository management commands")
+public class RepositoriesDispatcher extends DispatchCommand {
+
+	@Override
+	protected void setup(UserModel user) {
+		register(user, ListRepositories.class);
+	}
+
+	/* List repositories */
+	@CommandMetaData(name = "list", aliases = { "ls" }, description = "List repositories")
+	public static class ListRepositories extends SshCommand {
+
+		@Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
+		private boolean verbose;
+
+		@Override
+		public void run() {
+			IGitblit gitblit = getContext().getGitblit();
+			UserModel user = getContext().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)));
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/TicketsDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/TicketsDispatcher.java
index ca75a6c..f921798 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/TicketsDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/TicketsDispatcher.java
@@ -23,7 +23,7 @@
 public class TicketsDispatcher extends DispatchCommand {
 
 	@Override
-	protected void registerCommands(UserModel user) {
-		registerCommand(user, ReviewCommand.class);
+	protected void setup(UserModel user) {
+		register(user, ReviewCommand.class);
 	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java
new file mode 100644
index 0000000..0cbf354
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java
@@ -0,0 +1,106 @@
+/*
+ * 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.util.List;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.parboiled.common.StringUtils;
+
+import com.gitblit.manager.IGitblit;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.UserModel;
+import com.gitblit.transport.ssh.commands.CommandMetaData;
+import com.gitblit.transport.ssh.commands.DispatchCommand;
+import com.gitblit.transport.ssh.commands.SshCommand;
+
+@CommandMetaData(name = "users", description = "User management commands", admin = true)
+public class UsersDispatcher extends DispatchCommand {
+
+	@Override
+	protected void setup(UserModel user) {
+		register(user, ShowUser.class);
+		register(user, ListUsers.class);
+	}
+
+	@CommandMetaData(name = "show", description = "Show a user")
+	public static class ShowUser extends SshCommand {
+		@Argument(index = 0, required = true, metaVar = "USERNAME", usage = "username")
+		protected String username;
+
+		@Override
+		public void run() throws UnloggedFailure {
+			IGitblit gitblit = getContext().getGitblit();
+			UserModel user = gitblit.getUserModel(username);
+			if (user == null) {
+				throw new UnloggedFailure(1, String.format("Unknown user \"%s\"", username));
+			}
+			stdout.println();
+			stdout.println(user.username);
+			stdout.println();
+			for (RegistrantAccessPermission ap : user.getRepositoryPermissions()) {
+				stdout.println(String.format("%s %s", ap.registrant, ap.permission));
+			}
+		}
+	}
+
+	@CommandMetaData(name = "list", aliases= { "ls" }, description = "List users")
+	public static class ListUsers extends SshCommand {
+
+		@Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
+		private boolean verbose;
+
+		@Override
+		public void run() {
+			IGitblit gitblit = getContext().getGitblit();
+			List<UserModel> users = gitblit.getAllUsers();
+			int displaynameLen = 0;
+			int usernameLen = 0;
+			for (UserModel user : users) {
+				int len = user.getDisplayName().length();
+				if (len > displaynameLen) {
+					displaynameLen = len;
+				}
+				if (!StringUtils.isEmpty(user.username)) {
+					len = user.username.length();
+					if (len > usernameLen) {
+						usernameLen = len;
+					}
+				}
+			}
+
+			String pattern;
+			if (verbose) {
+				pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%-10s\t%s", displaynameLen, usernameLen);
+			} else {
+				pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s", displaynameLen, usernameLen);
+			}
+
+			for (UserModel user : users) {
+				if (user.disabled) {
+					continue;
+				}
+				stdout.println(String.format(pattern,
+						user.getDisplayName(),
+						(user.canAdmin() ? "*":" ") + user.username,
+						user.accountType,
+						user.emailAddress == null ? "" : user.emailAddress));
+			}
+		}
+	}
+}
\ No newline at end of file

--
Gitblit v1.9.1