From 617909819cd1b955647dd8584036fc7b2a014265 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 10 Apr 2014 18:58:10 -0400
Subject: [PATCH] Improve command help with formatting and usage examples

---
 src/main/java/com/gitblit/utils/FlipTable.java                              |    2 
 src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java         |    2 
 src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java   |    2 
 src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java      |    2 
 src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java           |   60 +++++
 src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java         |    7 
 src/main/java/com/gitblit/transport/ssh/gitblit/ConfigCommand.java          |    8 
 /dev/null                                                                   |   88 --------
 src/main/java/com/gitblit/transport/ssh/commands/UsageExamples.java         |   31 ++
 src/site/setup_transport_ssh.mkd                                            |   27 +-
 src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java |    4 
 src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java       |    4 
 src/main/java/com/gitblit/transport/ssh/WelcomeShell.java                   |   71 ++++-
 src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java        |  293 ++++++++++++++++++++++++--
 src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java            |    4 
 src/main/java/com/gitblit/transport/ssh/commands/UsageExample.java          |   32 ++
 16 files changed, 480 insertions(+), 157 deletions(-)

diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
index ccf2586..a9fe6f0 100644
--- a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
+++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
@@ -113,36 +113,71 @@
 		String getMessage() {
 			SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY);
 			UserModel user = client.getUser();
+			String hostname = getHostname();
+			int port = settings.getInteger(Keys.git.sshPort, 0);
+
+			final String b1 = StringUtils.rightPad("", 72, '═');
+			final String b2 = StringUtils.rightPad("", 72, '─');
+			final String nl = "\r\n";
 
 			StringBuilder msg = new StringBuilder();
-			msg.append("\r\n");
-			msg.append("Hi ");
+			msg.append(nl);
+			msg.append(b1);
+			msg.append(nl);
+			msg.append(" ");
+			msg.append(com.gitblit.Constants.getGitBlitVersion());
+			msg.append(nl);
+			msg.append(b1);
+			msg.append(nl);
+			msg.append(nl);
+			msg.append(" Hi ");
 			msg.append(user.getDisplayName());
-			msg.append(", you have successfully connected to Gitblit over SSH");
-			msg.append("\r\n");
-			msg.append("with client: ");
+			msg.append(", you have successfully connected over SSH.");
+			msg.append(nl);
+			msg.append(nl);
+			msg.append("   client: ");
 			msg.append(session.getClientVersion());
-			msg.append("\r\n");
-			msg.append("\r\n");
+			msg.append(nl);
+			msg.append(nl);
 
-			msg.append("You may clone a repository with the following Git syntax:\r\n");
-			msg.append("\r\n");
+			msg.append(b2);
+			msg.append(nl);
+			msg.append(nl);
+			msg.append(" You may clone a repository with the following Git syntax:");
+			msg.append(nl);
+			msg.append(nl);
 
 			msg.append("   git clone ");
-			msg.append(formatUrl(user.username));
-			msg.append("\r\n");
-			msg.append("\r\n");
+			msg.append(formatUrl(hostname, port, user.username));
+			msg.append(nl);
+			msg.append(nl);
+
+			msg.append(b2);
+			msg.append(nl);
+			msg.append(nl);
+
+			msg.append(" You may upload an SSH public key with the following syntax:");
+			msg.append(nl);
+			msg.append(nl);
+
+			msg.append(String.format("   cat ~/.ssh/id_rsa.pub | ssh -l %s -p %d %s gitblit keys add -", user.username, port, hostname));
+			msg.append(nl);
+			msg.append(nl);
+
+			msg.append(b2);
+			msg.append(nl);
+			msg.append(nl);
 
 			// display the core commands
 			SshCommandFactory cmdFactory = (SshCommandFactory) session.getFactoryManager().getCommandFactory();
 			DispatchCommand root = cmdFactory.createRootDispatcher(client, "");
-			String usage = root.usage().replace("\n", "\r\n");
+			String usage = root.usage().replace("\n", nl);
 			msg.append(usage);
 
 			return msg.toString();
 		}
 
-		private String formatUrl(String username) {
+		private String getHostname() {
 			String host = null;
 			String url = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443");
 			if (url != null) {
@@ -154,15 +189,17 @@
 			if (StringUtils.isEmpty(host)) {
 				host = SystemReader.getInstance().getHostname();
 			}
+			return host;
+		}
 
-			int port = settings.getInteger(Keys.git.sshPort, 0);
+		private String formatUrl(String hostname, int port, String username) {
 			if (port == 22) {
 				// standard port
-				return MessageFormat.format("{0}@{1}/REPOSITORY.git", username, host);
+				return MessageFormat.format("{0}@{1}/REPOSITORY.git", username, hostname);
 			} else {
 				// non-standard port
 				return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/REPOSITORY.git",
-						username, host, port);
+						username, hostname, port);
 			}
 		}
 	}
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 6a190df..d24a716 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
@@ -37,7 +37,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.gitblit.Keys;
 import com.gitblit.utils.IdGenerator;
+import com.gitblit.utils.StringUtils;
 import com.gitblit.utils.WorkQueue;
 import com.gitblit.utils.WorkQueue.CancelableRunnable;
 import com.gitblit.utils.cli.CmdLineParser;
@@ -200,9 +202,39 @@
 		}
 
 		if (clp.wasHelpRequestedByOption()) {
+			CommandMetaData meta = getClass().getAnnotation(CommandMetaData.class);
+			String title = meta.name().toUpperCase() + ": " + meta.description();
+			String b = com.gitblit.utils.StringUtils.leftPad("", title.length() + 2, '═');
 			StringWriter msg = new StringWriter();
-			clp.printDetailedUsage(commandName, msg);
-			msg.write(usage());
+			msg.write('\n');
+			msg.write(b);
+			msg.write('\n');
+			msg.write(' ');
+			msg.write(title);
+			msg.write('\n');
+			msg.write(b);
+			msg.write("\n\n");
+			msg.write("USAGE\n");
+			msg.write("─────\n");
+			msg.write(' ');
+			msg.write(commandName);
+			msg.write('\n');
+			msg.write(' ');
+			clp.printSingleLineUsage(msg, null);
+			msg.write("\n\n");
+			msg.write("ARGUMENTS & OPTIONS\n");
+			msg.write("───────────────────\n");
+			clp.printUsage(msg, null);
+			msg.write('\n');
+			String examples = usage().trim();
+			if (!StringUtils.isEmpty(examples)) {
+				msg.write('\n');
+				msg.write("EXAMPLES\n");
+				msg.write("────────\n");
+				msg.write(examples);
+				msg.write('\n');
+			}
+
 			throw new UnloggedFailure(1, msg.toString());
 		}
 	}
@@ -213,9 +245,33 @@
 	}
 
 	public String usage() {
+		Class<? extends BaseCommand> clazz = getClass();
+		if (clazz.isAnnotationPresent(UsageExamples.class)) {
+			return examples(clazz.getAnnotation(UsageExamples.class).examples());
+		} else if (clazz.isAnnotationPresent(UsageExample.class)) {
+			return examples(clazz.getAnnotation(UsageExample.class));
+		}
 		return "";
 	}
 
+	protected String examples(UsageExample... examples) {
+		int sshPort = getContext().getGitblit().getSettings().getInteger(Keys.git.sshPort, 29418);
+		String username = getContext().getClient().getUsername();
+		String hostname = "localhost";
+		String ssh = String.format("ssh -l %s -p %d %s", username, sshPort, hostname);
+
+		StringBuilder sb = new StringBuilder();
+		for (UsageExample example : examples) {
+			sb.append(example.description()).append("\n\n");
+			String syntax = example.syntax();
+			syntax = syntax.replace("${ssh}", ssh);
+			syntax = syntax.replace("${username}", username);
+			syntax = syntax.replace("${cmd}", commandName);
+			sb.append("   ").append(syntax).append("\n\n");
+		}
+		return sb.toString();
+	}
+
 	protected void showHelp() throws UnloggedFailure {
 		argv = new String [] { "--help" };
 		parseCommandLine();
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 b916bb1..6e9a87d 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
@@ -332,9 +332,9 @@
 				continue;
 			}
 
-			String displayName = name;
+			String displayName = name  + (meta.admin() ? "*" : "");
 			if (commandToAliases.containsKey(meta.name())) {
-				displayName = name + " (" + Joiner.on(',').join(commandToAliases.get(meta.name())) + ")";
+				displayName = name  + (meta.admin() ? "*" : "")+ " (" + Joiner.on(',').join(commandToAliases.get(meta.name())) + ")";
 			}
 			displayNames.put(name, displayName);
 
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java
index ee464e7..67e2805 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java
@@ -18,8 +18,12 @@
 import java.io.PrintWriter;
 
 import org.apache.sshd.server.Environment;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public abstract class SshCommand extends BaseCommand {
+
+	protected Logger log = LoggerFactory.getLogger(getClass());
 	protected PrintWriter stdout;
 	protected PrintWriter stderr;
 
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/UsageExample.java b/src/main/java/com/gitblit/transport/ssh/commands/UsageExample.java
new file mode 100644
index 0000000..428dfde
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/UsageExample.java
@@ -0,0 +1,32 @@
+/*
+ * 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 static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+* Annotation tagged on a concrete Command to describe how to use it.
+*/
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface UsageExample {
+String syntax();
+String description() default "";
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/UsageExamples.java b/src/main/java/com/gitblit/transport/ssh/commands/UsageExamples.java
new file mode 100644
index 0000000..0193a98
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/UsageExamples.java
@@ -0,0 +1,31 @@
+/*
+ * 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 static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+* Annotation tagged on a concrete Command to describe how to use it.
+*/
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface UsageExamples {
+UsageExample [] examples() default {};
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java b/src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java
index cc0c00a..5383786 100644
--- a/src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java
@@ -25,8 +25,10 @@
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.transport.ssh.commands.CommandMetaData;
+import com.gitblit.transport.ssh.commands.UsageExample;
 
 @CommandMetaData(name = "gc", description = "Cleanup unnecessary files and optimize the local repository", admin = true)
+@UsageExample(syntax = "${cmd} test/myrepository.git", description = "Garbage collect \"test/myrepository.git\"")
 public class GarbageCollectionCommand extends BaseGitCommand {
 
 	private static final Logger log = LoggerFactory.getLogger(GarbageCollectionCommand.class);
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
index 55a87e4..56f2c35 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java
@@ -36,7 +36,7 @@
 	protected List<String> readKeys(List<String> sshKeys)
 			throws UnsupportedEncodingException, IOException {
 		int idx = -1;
-		if (sshKeys.isEmpty() || ((idx = sshKeys.indexOf("-")) >= 0)) {
+		if ((idx = sshKeys.indexOf("-")) >= 0) {
 			String sshKey = "";
 			BufferedReader br = new BufferedReader(new InputStreamReader(
 					in, Charsets.UTF_8));
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ConfigCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ConfigCommand.java
index 695f085..f674034 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/ConfigCommand.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/ConfigCommand.java
@@ -14,9 +14,17 @@
 import com.gitblit.models.SettingModel;
 import com.gitblit.transport.ssh.commands.CommandMetaData;
 import com.gitblit.transport.ssh.commands.SshCommand;
+import com.gitblit.transport.ssh.commands.UsageExample;
+import com.gitblit.transport.ssh.commands.UsageExamples;
 import com.google.common.collect.Maps;
 
 @CommandMetaData(name = "config", description = "Administer Gitblit settings", admin = true)
+@UsageExamples(examples = {
+		@UsageExample(syntax = "${cmd} --list", description = "List all settings"),
+		@UsageExample(syntax = "${cmd} git.sshPort", description = "Describe the git.sshPort setting"),
+		@UsageExample(syntax = "${cmd} git.sshPort 29418", description = "Set git.sshPort to 29418"),
+		@UsageExample(syntax = "${cmd} git.sshPort --reset", description = "Reset git.sshPort to it's default value"),
+})
 public class ConfigCommand extends SshCommand {
 
 	@Argument(index = 0, metaVar = "KEY", usage = "The setting to describe or update")
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java
deleted file mode 100644
index 2917b6d..0000000
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java
+++ /dev/null
@@ -1,37 +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.gitblit;
-
-import org.kohsuke.args4j.Option;
-
-import com.gitblit.transport.ssh.commands.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 {
-
-  @Option(name = "--name", aliases = {"-n"}, required = true, metaVar = "NAME", usage = "name of repository to be created")
-  private String name;
-
-  @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of repository")
-  private String repositoryDescription;
-
-  @Override
-  public void run() {
-    stdout.println(String.format("Repository <%s> was created", name));
-  }
-}
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 86d8a8c..4200064 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java
@@ -26,8 +26,6 @@
 	protected void setup(UserModel user) {
 		// commands in this dispatcher
 		register(user, VersionCommand.class);
-		register(user, CreateRepository.class);
-		register(user, SetAccountCommand.class);
 		register(user, ConfigCommand.class);
 
 		// nested dispatchers
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 8c4aa22..61764c4 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java
@@ -30,6 +30,7 @@
 import com.gitblit.transport.ssh.commands.CommandMetaData;
 import com.gitblit.transport.ssh.commands.DispatchCommand;
 import com.gitblit.transport.ssh.commands.SshCommand;
+import com.gitblit.transport.ssh.commands.UsageExample;
 import com.gitblit.utils.FlipTable;
 import com.gitblit.utils.FlipTable.Borders;
 
@@ -50,11 +51,12 @@
 	}
 
 	@CommandMetaData(name = "add", description = "Add an SSH public key to your account")
+	@UsageExample(syntax = "cat ~/.ssh/id_rsa.pub | ${ssh} ${cmd} -", description = "Upload your SSH public key and add it to your account")
 	public static class AddKey extends BaseKeyCommand {
 
 		protected final Logger log = LoggerFactory.getLogger(getClass());
 
-		@Argument(metaVar = "<stdin>|KEY", usage = "the key to add")
+		@Argument(metaVar = "-|<KEY>", usage = "the key(s) to add", required = true)
 		private List<String> addKeys = new ArrayList<String>();
 
 		@Override
@@ -70,6 +72,7 @@
 	}
 
 	@CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove an SSH public key from your account")
+	@UsageExample(syntax = "${cmd} 2", description = "Remove the SSH key identified as #2 in `keys list`")
 	public static class RemoveKey extends BaseKeyCommand {
 
 		protected final Logger log = LoggerFactory.getLogger(getClass());
@@ -131,7 +134,7 @@
 		}
 	}
 
-	@CommandMetaData(name = "list", aliases = { "ls" }, description = "List your registered public keys")
+	@CommandMetaData(name = "list", aliases = { "ls" }, description = "List your registered SSH public keys")
 	public static class ListKeys extends SshCommand {
 
 		@Option(name = "-L", usage = "list complete public key parameters")
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
index 4be60ab..f2fbabb 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java
@@ -23,6 +23,7 @@
 import com.gitblit.transport.ssh.commands.CommandMetaData;
 import com.gitblit.transport.ssh.commands.DispatchCommand;
 import com.gitblit.transport.ssh.commands.ListFilterCommand;
+import com.gitblit.transport.ssh.commands.UsageExample;
 import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.FlipTable;
 import com.gitblit.utils.FlipTable.Borders;
@@ -38,6 +39,7 @@
 
 	/* List repositories */
 	@CommandMetaData(name = "list", aliases = { "ls" }, description = "List repositories")
+	@UsageExample(syntax = "${cmd} mirror/.* -v", description = "Verbose list of all repositories in the 'mirror' directory")
 	public static class ListRepositories extends ListFilterCommand<RepositoryModel> {
 
 		@Override
@@ -72,7 +74,7 @@
 				String size = r.size;
 				if (!r.hasCommits) {
 					lm = "";
-					size = "(empty)";
+					size = FlipTable.EMPTY;
 				}
 				if (verbose) {
 					String owners = "";
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java
deleted file mode 100644
index 3f98778..0000000
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java
+++ /dev/null
@@ -1,88 +0,0 @@
-//Copyright (C) 2012 The Android Open Source Project
-//
-//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.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
-import com.gitblit.transport.ssh.SshKey;
-import com.gitblit.transport.ssh.commands.CommandMetaData;
-
-/** Set a user's account settings. **/
-@CommandMetaData(name = "set-account", description = "Change an account's settings", admin = true)
-public class SetAccountCommand extends BaseKeyCommand {
-
-	private static final String ALL = "ALL";
-
-	@Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
-	private String user;
-
-	@Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account")
-	private List<String> addSshKeys = new ArrayList<String>();
-
-	@Option(name = "--delete-ssh-key", metaVar = "-|KEY", usage = "public keys to delete from the account")
-	private List<String> deleteSshKeys = new ArrayList<String>();
-
-	@Override
-	public void run() throws IOException, UnloggedFailure {
-		validate();
-		setAccount();
-	}
-
-	private void validate() throws UnloggedFailure {
-		if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
-			throw new UnloggedFailure(1, "Only one option may use the stdin");
-		}
-		if (deleteSshKeys.contains(ALL)) {
-			deleteSshKeys = Collections.singletonList(ALL);
-		}
-	}
-
-	private void setAccount() throws IOException, UnloggedFailure {
-		addSshKeys = readKeys(addSshKeys);
-		if (!addSshKeys.isEmpty()) {
-			addSshKeys(addSshKeys);
-		}
-
-		deleteSshKeys = readKeys(deleteSshKeys);
-		if (!deleteSshKeys.isEmpty()) {
-			deleteSshKeys(deleteSshKeys);
-		}
-	}
-
-	private void addSshKeys(List<String> keys) throws UnloggedFailure,
-			IOException {
-		for (String key : keys) {
-			SshKey sshKey = new SshKey(key);
-			getKeyManager().addKey(user, sshKey);
-		}
-	}
-
-	private void deleteSshKeys(List<String> keys) {
-		if (keys.contains(ALL)) {
-			getKeyManager().removeAllKeys(user);
-		} else {
-			for (String key : keys) {
-				SshKey sshKey = new SshKey(key);
-				getKeyManager().removeKey(user, sshKey);
-			}
-		}
-	}
-}
diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java
index bed966d..d892d9a 100644
--- a/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java
@@ -15,75 +15,308 @@
  */
 package com.gitblit.transport.ssh.gitblit;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
 
+import com.gitblit.Constants.AccessPermission;
 import com.gitblit.manager.IGitblit;
 import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryModel;
 import com.gitblit.models.TeamModel;
 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.ListFilterCommand;
 import com.gitblit.transport.ssh.commands.SshCommand;
+import com.gitblit.transport.ssh.commands.UsageExample;
+import com.gitblit.transport.ssh.commands.UsageExamples;
+import com.gitblit.utils.ArrayUtils;
 import com.gitblit.utils.FlipTable;
 import com.gitblit.utils.FlipTable.Borders;
+import com.gitblit.utils.StringUtils;
+import com.google.common.base.Joiner;
 
 @CommandMetaData(name = "users", description = "User management commands", admin = true)
 public class UsersDispatcher extends DispatchCommand {
 
 	@Override
 	protected void setup(UserModel user) {
+		// primary user commands
+		register(user, NewUser.class);
+		register(user, RemoveUser.class);
 		register(user, ShowUser.class);
 		register(user, ListUsers.class);
+
+		// user-specific commands
+		register(user, SetName.class);
+		register(user, Permissions.class);
+		register(user, DisableUser.class);
+		register(user, EnableUser.class);
 	}
 
-	@CommandMetaData(name = "show", description = "Show a user")
-	public static class ShowUser extends SshCommand {
+	public static abstract class UserCommand extends SshCommand {
 		@Argument(index = 0, required = true, metaVar = "USERNAME", usage = "username")
 		protected String username;
+
+		protected UserModel getUser(boolean requireUser) throws UnloggedFailure {
+			IGitblit gitblit = getContext().getGitblit();
+			UserModel user = gitblit.getUserModel(username);
+			if (requireUser && user == null) {
+				throw new UnloggedFailure(1, String.format("User %s does not exist!", username));
+			}
+			return user;
+		}
+	}
+
+	@CommandMetaData(name = "new", description = "Create a new user account")
+	@UsageExample(syntax = "${cmd} john 12345 --email john@smith.com --canFork --canCreate")
+	public static class NewUser extends UserCommand {
+
+		@Argument(index = 1, required = true, metaVar = "PASSWORD", usage = "password")
+		protected String password;
+
+		@Option(name = "--email", metaVar = "ADDRESS", usage = "email address")
+		protected String email;
+
+		@Option(name = "--canAdmin", usage = "can administer the server")
+		protected boolean canAdmin;
+
+		@Option(name = "--canFork", usage = "can fork repositories")
+		protected boolean canFork;
+
+		@Option(name = "--canCreate", usage = "can create personal repositories")
+		protected boolean canCreate;
+
+		@Option(name = "--disabled", usage = "create a disabled user account")
+		protected boolean disabled;
+
+		@Override
+		public void run() throws UnloggedFailure {
+
+			if (getUser(false) != null) {
+				throw new UnloggedFailure(1, String.format("User %s already exists!", username));
+			}
+
+			UserModel user = new UserModel(username);
+			user.password = password;
+
+			if (email != null) {
+				user.emailAddress = email;
+			}
+
+			user.canAdmin = canAdmin;
+			user.canFork = canFork;
+			user.canCreate = canCreate;
+			user.disabled = disabled;
+
+			IGitblit gitblit = getContext().getGitblit();
+			if (gitblit.updateUserModel(username, user)) {
+				stdout.println(String.format("%s created.", username));
+			} else {
+				throw new UnloggedFailure(1, String.format("Failed to create %s!", username));
+			}
+		}
+	}
+
+	@CommandMetaData(name = "set-name", description = "Set the display name of an account")
+	@UsageExample(syntax = "${cmd} john John Smith", description = "The display name to \"John Smith\" for john's account")
+	public static class SetName extends UserCommand {
+
+		@Argument(index = 1, multiValued = true, required = true, metaVar = "NAME", usage = "display name")
+		protected List<String> displayName = new ArrayList<String>();
+
+		@Override
+		public void run() throws UnloggedFailure {
+			UserModel user = getUser(true);
+
+			IGitblit gitblit = getContext().getGitblit();
+			user.displayName = Joiner.on(" ").join(displayName);
+			if (gitblit.updateUserModel(username, user)) {
+				stdout.println(String.format("Set the display name of %s to \"%s\".", username, user.displayName));
+			} else {
+				throw new UnloggedFailure(1, String.format("Failed to set the display name of %s!", username));
+			}
+		}
+	}
+
+	@CommandMetaData(name = "disable", description = "Prohibit an account from authenticating")
+	@UsageExample(syntax = "${cmd} john", description = "Prevent John from authenticating")
+	public static class DisableUser extends UserCommand {
+
+		@Override
+		public void run() throws UnloggedFailure {
+
+			UserModel user = getUser(true);
+			user.disabled = true;
+
+			IGitblit gitblit = getContext().getGitblit();
+			if (gitblit.updateUserModel(username, user)) {
+				stdout.println(String.format("%s is not allowed to authenticate.", username));
+			} else {
+				throw new UnloggedFailure(1, String.format("Failed to disable %s!", username));
+			}
+		}
+	}
+
+	@CommandMetaData(name = "enable", description = "Allow an account to authenticate")
+	@UsageExample(syntax = "${cmd} john", description = "Allow John to authenticate")
+	public static class EnableUser extends UserCommand {
+
+		@Override
+		public void run() throws UnloggedFailure {
+
+			UserModel user = getUser(true);
+			user.disabled = false;
+
+			IGitblit gitblit = getContext().getGitblit();
+			if (gitblit.updateUserModel(username, user)) {
+				stdout.println(String.format("%s may now authenticate.", username));
+			} else {
+				throw new UnloggedFailure(1, String.format("Failed to enable %s!", username));
+			}
+		}
+	}
+
+	@CommandMetaData(name = "permissions", aliases = { "perms" }, description = "Add or remove permissions from an account")
+	@UsageExample(syntax = "${cmd} john RW:alpha/repo.git RWC:alpha/repo2.git", description = "Add or set permissions for John")
+	public static class Permissions extends UserCommand {
+
+		@Argument(index = 1, multiValued = true, metaVar = "[PERMISSION:]REPOSITORY", usage = "a repository expression")
+		protected List<String> permissions;
+
+		@Option(name = "--remove", aliases = { "-r" }, metaVar = "REPOSITORY|ALL", usage = "remove a repository permission")
+		protected List<String> removals;
 
 		@Override
 		public void run() throws UnloggedFailure {
 			IGitblit gitblit = getContext().getGitblit();
-			UserModel u = gitblit.getUserModel(username);
-			if (u == null) {
-				throw new UnloggedFailure(1, String.format("Unknown user \"%s\"", username));
+			UserModel user = getUser(true);
+
+			boolean modified = false;
+			if (!ArrayUtils.isEmpty(removals)) {
+				if (removals.contains("ALL")) {
+					user.permissions.clear();
+				} else {
+					for (String repo : removals) {
+						user.removeRepositoryPermission(repo);
+						log.info(String.format("Removing permission for %s from %s", repo, username));
+					}
+				}
+				modified = true;
 			}
+
+			if (!ArrayUtils.isEmpty(permissions)) {
+				for (String perm : permissions) {
+					String repo = AccessPermission.repositoryFromRole(perm);
+					if (StringUtils.findInvalidCharacter(repo) == null) {
+						// explicit permision, confirm repository
+						RepositoryModel r = gitblit.getRepositoryModel(repo);
+						if (r == null) {
+							throw new UnloggedFailure(1, String.format("Repository %s does not exist!", repo));
+						}
+					}
+					AccessPermission ap = AccessPermission.permissionFromRole(perm);
+					user.setRepositoryPermission(repo, ap);
+					log.info(String.format("Setting %s:%s for %s", ap.name(), repo, username));
+				}
+				modified = true;
+			}
+
+			if (modified && gitblit.updateUserModel(username, user)) {
+				// reload & display new permissions
+				user = gitblit.getUserModel(username);
+			}
+
+			showPermissions(user);
+		}
+
+		protected void showPermissions(UserModel user) {
+			List<RegistrantAccessPermission> perms = user.getRepositoryPermissions();
+			String[] pheaders = { "Repository", "Permission", "Type", "Source", "Mutable" };
+			Object [][] pdata = new Object[perms.size()][];
+			for (int i = 0; i < perms.size(); i++) {
+				RegistrantAccessPermission ap = perms.get(i);
+				pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"" };
+			}
+			stdout.println(FlipTable.of(pheaders, pdata, Borders.BODY_HCOLS));
+		}
+	}
+
+	@CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove a user account")
+	@UsageExample(syntax = "${cmd} john", description = "Delete john's account")
+	public static class RemoveUser extends UserCommand {
+
+		@Override
+		public void run() throws UnloggedFailure {
+
+			UserModel user = getUser(true);
+			IGitblit gitblit = getContext().getGitblit();
+			if (gitblit.deleteUserModel(user)) {
+				stdout.println(String.format("%s has been deleted.", username));
+			} else {
+				throw new UnloggedFailure(1, String.format("Failed to delete %s!", username));
+			}
+		}
+	}
+
+	@CommandMetaData(name = "show", description = "Show the details of an account")
+	@UsageExample(syntax = "${cmd} john", description = "Display john's account")
+	public static class ShowUser extends UserCommand {
+
+		@Override
+		public void run() throws UnloggedFailure {
+
+			UserModel u = getUser(true);
 
 			// fields
 			String [] fheaders = new String [] { "Field", "Value" };
 			Object [][] fdata = new Object[5][];
 			fdata[0] = new Object [] { "Email", u.emailAddress };
 			fdata[1] = new Object [] { "Type", u.accountType };
-			fdata[2] = new Object [] { "Can Admin", u.canAdmin() ? "Y":"N" };
-			fdata[3] = new Object [] { "Can Fork", u.canFork() ? "Y":"N" };
-			fdata[4] = new Object [] { "Can Create", u.canCreate() ? "Y":"N" };
+			fdata[2] = new Object [] { "Can Admin", u.canAdmin() ? "Y":"" };
+			fdata[3] = new Object [] { "Can Fork", u.canFork() ? "Y":"" };
+			fdata[4] = new Object [] { "Can Create", u.canCreate() ? "Y":"" };
 			String fields = FlipTable.of(fheaders, fdata, Borders.COLS);
 
 			// teams
-			String [] theaders = new String [] { "Team", "Type" };
-			Object [][] tdata = new Object[u.teams.size()][];
-			int i = 0;
-			for (TeamModel t : u.teams) {
-				tdata[i] = new Object [] { t.name, t.accountType };
-				i++;
+			String teams;
+			if (u.teams.size() == 0) {
+				teams = FlipTable.EMPTY;
+			} else {
+				String [] theaders = new String [] { "Team", "Type" };
+				Object [][] tdata = new Object[u.teams.size()][];
+				int i = 0;
+				for (TeamModel t : u.teams) {
+					tdata[i] = new Object [] { t.name, t.accountType };
+					i++;
+				}
+				teams = FlipTable.of(theaders, tdata, Borders.COLS);
 			}
-			String teams = FlipTable.of(theaders, tdata, Borders.COLS);
 
 			// permissions
 			List<RegistrantAccessPermission> perms = u.getRepositoryPermissions();
-			String[] pheaders = { "Repository", "Permission", "Type", "Source", "Mutable" };
-			Object [][] pdata = new Object[perms.size()][];
-			for (i = 0; i < perms.size(); i++) {
-				RegistrantAccessPermission ap = perms.get(i);
-				pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"N" };
+			String permissions;
+			if (perms.isEmpty()) {
+				permissions = FlipTable.EMPTY;
+			} else {
+				String[] pheaders = { "Repository", "Permission", "Type", "Source", "Mutable" };
+				Object [][] pdata = new Object[perms.size()][];
+				for (int i = 0; i < perms.size(); i++) {
+					RegistrantAccessPermission ap = perms.get(i);
+					pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"" };
+				}
+				permissions = FlipTable.of(pheaders, pdata, Borders.COLS);
 			}
-			String permissions = FlipTable.of(pheaders, pdata, Borders.COLS);
 
 			// assemble user table
-			String [] headers = new String[] { u.getDisplayName() + (u.username.equals(u.getDisplayName()) ? "" : (" (" + u.username + ")")) };
+			String userTitle = u.getDisplayName() + (u.username.equals(u.getDisplayName()) ? "" : (" (" + u.username + ")"));
+			if (u.disabled) {
+				userTitle += "  [DISABLED]";
+			}
+			String [] headers = new String[] { userTitle };
 			String[][] data = new String[6][];
 			data[0] = new String [] { "FIELDS" };
 			data[1] = new String [] { fields };
@@ -95,7 +328,11 @@
 		}
 	}
 
-	@CommandMetaData(name = "list", aliases= { "ls" }, description = "List users")
+	@CommandMetaData(name = "list", aliases= { "ls" }, description = "List accounts")
+	@UsageExamples( examples = {
+		@UsageExample(syntax = "${cmd}", description = "List accounts as a table"),
+		@UsageExample(syntax = "${cmd} j.*", description = "List all accounts that start with 'j'"),
+	})
 	public static class ListUsers extends ListFilterCommand<UserModel> {
 
 		@Override
@@ -125,10 +362,12 @@
 			for (int i = 0; i < list.size(); i++) {
 				UserModel u = list.get(i);
 
-				String name = u.disabled ? "-" : ((u.canAdmin() ? "*" : " ")) + u.username;
+				String name = (u.disabled ? "-" : ((u.canAdmin() ? "*" : " "))) + u.username;
 				if (verbose) {
 					data[i] = new Object[] { name, u.displayName, u.accountType,
-							u.emailAddress,	u.canCreate() ? "Y":"", u.canFork() ? "Y" : ""};
+							u.emailAddress,
+							(u.canAdmin() || u.canCreate()) ? "Y":"",
+							(u.canAdmin() || u.canFork()) ? "Y" : ""};
 				} else {
 					data[i] = new Object[] { name, u.displayName, u.accountType,
 							u.emailAddress };
@@ -147,8 +386,8 @@
 							u.getDisplayName(),
 							u.accountType,
 							u.emailAddress == null ? "" : u.emailAddress,
-							u.canCreate() ? "Y":"",
-							u.canFork() ? "Y" : "");
+							(u.canAdmin() || u.canCreate()) ? "Y":"",
+							(u.canAdmin() || u.canFork()) ? "Y" : "");
 				}
 			} else {
 				for (UserModel u : users) {
diff --git a/src/main/java/com/gitblit/utils/FlipTable.java b/src/main/java/com/gitblit/utils/FlipTable.java
index 7aa5f0b..0197517 100644
--- a/src/main/java/com/gitblit/utils/FlipTable.java
+++ b/src/main/java/com/gitblit/utils/FlipTable.java
@@ -36,7 +36,7 @@
  * </pre>
  */
 public final class FlipTable {
-	private static final String EMPTY = "(empty)";
+	public static final String EMPTY = "(empty)";
 
 	public static enum Borders {
 		FULL(15), BODY_HCOLS(13), HCOLS(12), BODY(9), HEADER(8), COLS(4);
diff --git a/src/site/setup_transport_ssh.mkd b/src/site/setup_transport_ssh.mkd
index c3d97a3..5bac2ff 100644
--- a/src/site/setup_transport_ssh.mkd
+++ b/src/site/setup_transport_ssh.mkd
@@ -23,14 +23,14 @@
 
 Then you can upload your *public* key right from the command-line.
 
-    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add
-    cat c:\<userfolder>\.ssh\id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add
+    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -
+    cat c:\<userfolder>\.ssh\id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -
 
 **NOTE:** It is important to note that *ssh-keygen* generates a public/private keypair (e.g. id_rsa and id_rsa.pub).  You want to upload the *public* key, which is denoted by the *.pub* file extension.
 
 Once you've done both of those steps you should be able to execute the following command without a password prompt.
 
-    ssh -l <username> -p 29418 <hostname> gitblit version
+    ssh -l <username> -p 29418 <hostname>
 
 ### Setting up an SSH alias
 
@@ -40,7 +40,7 @@
 
 You can define an alias for your server which will reduce your command syntax to something like this.
 
-    ssh <alias> gitblit version
+    ssh <alias>
 
 Create or modify your `~/.ssh/config` file and add a host entry.  If you are on Windows, you'll want to create or modify `<userfolder>\.ssh\config`, where *userfolder* is dependent on your version of Windows.  Most recently this is `c:\users\<userfolder>`.
 
@@ -62,22 +62,21 @@
 
 Add an SSH public key to your account.  This command accepts a public key piped to stdin.
 
-    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add
+    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys add -
+
+##### keys list
+
+Show the SSH public keys you have added to your account.
+
+    ssh -l <username> -p 29418 <hostname> gitblit keys list
 
 ##### keys remove
 
-Remove an SSH public key from your account.  This command accepts a public key piped to stdin.
+Remove an SSH public key from your account.  This command accepts several input values, the most useful one is an index number which matches the index number displayed in the `list` command.
 
-    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> gitblit keys remove
+    ssh -l <username> -p 29418 <hostname> gitblit keys remove 2
 
 You can also remove all your public keys from your account.
 
     ssh -l <username> -p 29418 <hostname> gitblit keys remove ALL
-
-##### keys list
-
-Show the SSH keys you have added to your account.
-
-    ssh -l <username> -p 29418 <hostname> gitblit keys list
-
 

--
Gitblit v1.9.1