Display command description in ssh gerrit --help

Show one line description for each ssh command. Make COMMAND
argument optional, so that --help argument is not needed.

This approach is working for core gerrit commands, aliases, nested
commands and plugin's own ssh commands.

To provide a description ssh command must be annotated with
CommandMetaData annotation, i. e.:

@CommandMetaData(name="print", descr="Print greeting in different languages")
public final class PrintHelloWorldCommand extends SshCommand { ...

Syntactic sugar for command registration is provided to reflect the fact,
that both name and description are included in CommandMetaData annotation.

To register command and alias in plugin:
protected void configureCommands() {
  command(PrintHelloWorldCommand.class);
  alias("say-hello", PrintHelloWorldCommand.class);
[...]

With the outcome:
$ ssh gerrit helloworld
Available commands of helloworld are:

   print       Print greeting in different languages
   say-hello   Print greeting in different languages

Change-Id: I2e5440378023ecc5425092d8131f121da2f20a30
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
index 9582c93..b46fced 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -60,26 +60,26 @@
   }
 
   private void begin(Environment env) throws UnloggedFailure, IOException {
-    Map<String, Provider<Command>> map = root.getMap();
+    Map<String, CommandProvider> map = root.getMap();
     for (String name : chain(command)) {
-      Provider<? extends Command> p = map.get(name);
+      CommandProvider p = map.get(name);
       if (p == null) {
         throw new UnloggedFailure(1, getName() + ": not found");
       }
 
-      Command cmd = p.get();
+      Command cmd = p.getProvider().get();
       if (!(cmd instanceof DispatchCommand)) {
         throw new UnloggedFailure(1, getName() + ": not found");
       }
       map = ((DispatchCommand) cmd).getMap();
     }
 
-    Provider<? extends Command> p = map.get(command.value());
+    CommandProvider p = map.get(command.value());
     if (p == null) {
       throw new UnloggedFailure(1, getName() + ": not found");
     }
 
-    Command cmd = p.get();
+    Command cmd = p.getProvider().get();
     checkRequiresCapability(cmd);
     if (cmd instanceof BaseCommand) {
       BaseCommand bc = (BaseCommand)cmd;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
new file mode 100644
index 0000000..cfcee6a
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 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.google.gerrit.sshd;
+
+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 what it is doing
+ */
+@Target( {ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface CommandMetaData {
+  String name();
+  String descr() default "";
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
index 7699bdd..e7e8a44 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
@@ -48,7 +48,7 @@
   /**
    * Configure a command to be invoked by name.
    *
-   *@param parent context of the parent command, that this command is a
+   * @param parent context of the parent command, that this command is a
    *        subcommand of.
    * @param name the name of the command the client will provide in order to
    *        call the command.
@@ -61,6 +61,42 @@
   }
 
   /**
+   * Configure a command to be invoked by name. The command is bound to the passed class.
+   *
+   * @param parent context of the parent command, that this command is a
+   *        subcommand of.
+   * @param clazz class of the command with {@link CommandMetaData} annotation
+   *        to retrieve the name and the description from
+   */
+  protected void command(final CommandName parent,
+      final Class<? extends BaseCommand> clazz) {
+    CommandMetaData meta = (CommandMetaData)clazz.getAnnotation(CommandMetaData.class);
+    if (meta == null) {
+      throw new IllegalStateException("no CommandMetaData annotation found");
+    }
+    bind(Commands.key(parent, meta.name(), meta.descr())).to(clazz);
+  }
+
+  /**
+   * Alias one command to another. The alias is bound to the passed class.
+   *
+   * @param parent context of the parent command, that this command is a
+   *        subcommand of.
+   * @param name the name of the command the client will provide in order to
+   *        call the command.
+   * @param clazz class of the command with {@link CommandMetaData} annotation
+   *        to retrieve the description from
+   */
+  protected void alias(final CommandName parent, final String name,
+      final Class<? extends BaseCommand> clazz) {
+    CommandMetaData meta = (CommandMetaData)clazz.getAnnotation(CommandMetaData.class);
+    if (meta == null) {
+      throw new IllegalStateException("no CommandMetaData annotation found");
+    }
+    bind(Commands.key(parent, name, meta.descr())).to(clazz);
+  }
+
+  /**
    * Alias one command to another.
    *
    * @param from the new command name that when called will actually delegate to
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java
new file mode 100644
index 0000000..9a7c97b
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2013 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.google.gerrit.sshd;
+
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+
+final class CommandProvider {
+
+  private final Provider<Command> provider;
+  private final String description;
+
+  CommandProvider(final Provider<Command> p, final String d) {
+    this.provider = p;
+    this.description = d;
+  }
+
+  public Provider<Command> getProvider() {
+    return provider;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 5340d6f..929a895 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -16,6 +16,7 @@
 
 import com.google.inject.Key;
 
+import org.apache.commons.lang.StringUtils;
 import org.apache.sshd.server.Command;
 
 import java.lang.annotation.Annotation;
@@ -41,6 +42,11 @@
     return Key.get(Command.class, named(parent, name));
   }
 
+  public static Key<Command> key(final CommandName parent,
+      final String name, final String descr) {
+    return Key.get(Command.class, named(parent, name, descr));
+  }
+
   /** Create a CommandName annotation for the supplied name. */
   public static CommandName named(final String name) {
     return new CommandName() {
@@ -78,6 +84,12 @@
     return new NestedCommandNameImpl(parent, name);
   }
 
+  /** Create a CommandName annotation for the supplied name and description. */
+  public static CommandName named(final CommandName parent, final String name,
+      final String descr) {
+    return new NestedCommandNameImpl(parent, name, descr);
+  }
+
   /** Return the name of this command, possibly including any parents. */
   public static String nameOf(final CommandName name) {
     if (name instanceof NestedCommandNameImpl) {
@@ -104,13 +116,22 @@
     return null;
   }
 
-  private static final class NestedCommandNameImpl implements CommandName {
+  static final class NestedCommandNameImpl implements CommandName {
     private final CommandName parent;
     private final String name;
+    private final String descr;
 
     NestedCommandNameImpl(final CommandName parent, final String name) {
       this.parent = parent;
       this.name = name;
+      this.descr = StringUtils.EMPTY;
+    }
+
+    NestedCommandNameImpl(final CommandName parent, final String name,
+        final String descr) {
+      this.parent = parent;
+      this.name = name;
+      this.descr = descr;
     }
 
     @Override
@@ -118,6 +139,10 @@
       return name;
     }
 
+    public String descr() {
+      return descr;
+    }
+
     @Override
     public Class<? extends Annotation> annotationType() {
       return CommandName.class;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 691f3a0..455b732 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -29,6 +30,7 @@
 import org.kohsuke.args4j.Argument;
 
 import java.io.IOException;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -39,14 +41,14 @@
  */
 final class DispatchCommand extends BaseCommand {
   interface Factory {
-    DispatchCommand create(Map<String, Provider<Command>> map);
+    DispatchCommand create(Map<String, CommandProvider> map);
   }
 
   private final Provider<CurrentUser> currentUser;
-  private final Map<String, Provider<Command>> commands;
+  private final Map<String, CommandProvider> commands;
   private final AtomicReference<Command> atomicCmd;
 
-  @Argument(index = 0, required = true, metaVar = "COMMAND", handler = SubcommandHandler.class)
+  @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
   private String commandName;
 
   @Argument(index = 1, multiValued = true, metaVar = "ARG")
@@ -54,13 +56,13 @@
 
   @Inject
   DispatchCommand(final Provider<CurrentUser> cu,
-      @Assisted final Map<String, Provider<Command>> all) {
+      @Assisted final Map<String, CommandProvider> all) {
     currentUser = cu;
     commands = all;
     atomicCmd = Atomics.newReference();
   }
 
-  Map<String, Provider<Command>> getMap() {
+  Map<String, CommandProvider> getMap() {
     return commands;
   }
 
@@ -68,8 +70,13 @@
   public void start(final Environment env) throws IOException {
     try {
       parseCommandLine();
+      if (Strings.isNullOrEmpty(commandName)) {
+        StringWriter msg = new StringWriter();
+        msg.write(usage());
+        throw new UnloggedFailure(1, msg.toString());
+      }
 
-      final Provider<Command> p = commands.get(commandName);
+      final CommandProvider p = commands.get(commandName);
       if (p == null) {
         String msg =
             (getName().isEmpty() ? "Gerrit Code Review" : getName()) + ": "
@@ -77,7 +84,7 @@
         throw new UnloggedFailure(1, msg);
       }
 
-      final Command cmd = p.get();
+      final Command cmd = p.getProvider().get();
       checkRequiresCapability(cmd);
       if (cmd instanceof BaseCommand) {
         final BaseCommand bc = (BaseCommand) cmd;
@@ -138,9 +145,17 @@
     }
     usage.append(" are:\n");
     usage.append("\n");
+
+    int maxLength = -1;
+    for (String name : commands.keySet()) {
+      maxLength = Math.max(maxLength, name.length());
+    }
+    String format = "%-" + maxLength + "s   %s";
     for (String name : Sets.newTreeSet(commands.keySet())) {
+      final CommandProvider p = commands.get(name);
       usage.append("   ");
-      usage.append(name);
+      usage.append(String.format(format, name,
+          Strings.nullToEmpty(p.getDescription())));
       usage.append("\n");
     }
     usage.append("\n");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index b76ff71..58f9d22 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -39,7 +39,7 @@
   private DispatchCommand.Factory factory;
 
   private final CommandName parent;
-  private volatile ConcurrentMap<String, Provider<Command>> map;
+  private volatile ConcurrentMap<String, CommandProvider> map;
 
   public DispatchCommandProvider(final CommandName cn) {
     this.parent = cn;
@@ -52,8 +52,8 @@
 
   public RegistrationHandle register(final CommandName name,
       final Provider<Command> cmd) {
-    final ConcurrentMap<String, Provider<Command>> m = getMap();
-    if (m.putIfAbsent(name.value(), cmd) != null) {
+    final ConcurrentMap<String, CommandProvider> m = getMap();
+    if (m.putIfAbsent(name.value(), new CommandProvider(cmd, null)) != null) {
       throw new IllegalArgumentException(name.value() + " exists");
     }
     return new RegistrationHandle() {
@@ -66,8 +66,8 @@
 
   public RegistrationHandle replace(final CommandName name,
       final Provider<Command> cmd) {
-    final ConcurrentMap<String, Provider<Command>> m = getMap();
-    m.put(name.value(), cmd);
+    final ConcurrentMap<String, CommandProvider> m = getMap();
+    m.put(name.value(), new CommandProvider(cmd, null));
     return new RegistrationHandle() {
       @Override
       public void remove() {
@@ -76,7 +76,7 @@
     };
   }
 
-  ConcurrentMap<String, Provider<Command>> getMap() {
+  ConcurrentMap<String, CommandProvider> getMap() {
     if (map == null) {
       synchronized (this) {
         if (map == null) {
@@ -88,14 +88,21 @@
   }
 
   @SuppressWarnings("unchecked")
-  private ConcurrentMap<String, Provider<Command>> createMap() {
-    ConcurrentMap<String, Provider<Command>> m = Maps.newConcurrentMap();
+  private ConcurrentMap<String, CommandProvider> createMap() {
+    ConcurrentMap<String, CommandProvider> m = Maps.newConcurrentMap();
     for (final Binding<?> b : allCommands()) {
       final Annotation annotation = b.getKey().getAnnotation();
       if (annotation instanceof CommandName) {
+        String descr = null;
+        if (annotation instanceof Commands.NestedCommandNameImpl) {
+          Commands.NestedCommandNameImpl impl =
+              ((Commands.NestedCommandNameImpl) annotation);
+          descr = impl.descr();
+        }
         final CommandName n = (CommandName) annotation;
         if (!Commands.CMD_ROOT.equals(n) && Commands.isChild(parent, n)) {
-          m.put(n.value(), (Provider<Command>) b.getProvider());
+          m.put(n.value(),
+              new CommandProvider((Provider<Command>) b.getProvider(), descr));
         }
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
index 4dbb8d7..013f070 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -16,21 +16,17 @@
 
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.sshd.CommandName;
-import com.google.gerrit.sshd.Commands;
-import com.google.gerrit.sshd.DispatchCommandProvider;
-import com.google.inject.AbstractModule;
 import com.google.inject.binder.LinkedBindingBuilder;
 
 import org.apache.sshd.server.Command;
 
 import javax.inject.Inject;
 
-public abstract class PluginCommandModule extends AbstractModule {
+public abstract class PluginCommandModule extends CommandModule {
   private CommandName command;
 
   @Inject
-  void setPluginName(@PluginName String name) {
+  void setPluginName(@PluginName String name, final String descr) {
     this.command = Commands.named(name);
   }
 
@@ -46,4 +42,13 @@
   protected LinkedBindingBuilder<Command> command(String subCmd) {
     return bind(Commands.key(command, subCmd));
   }
+
+  protected void command(Class<? extends BaseCommand> clazz) {
+    command(command, clazz);
+  }
+
+  protected void alias(final String name, Class<? extends BaseCommand> clazz) {
+    alias(command, name, clazz);
+  }
+
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 08c650c..0ca8038 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -25,6 +26,7 @@
 /** Opens a query processor. */
 @AdminHighPriorityCommand
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "gsql", descr = "Administrative interface to active database")
 final class AdminQueryShell extends SshCommand {
   @Inject
   private QueryShell.Factory factory;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 6483e24..1529314 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -40,6 +41,7 @@
 import java.util.Set;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "set-project-parent", descr = "Change the project permissions are inherited from")
 final class AdminSetParent extends SshCommand {
   private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 939d68a..0268bc0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.git.BanCommitResult;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -32,6 +33,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+@CommandMetaData(name = "ban-commit", descr = "Ban a commit from a project's repository")
 public class BanCommitCommand extends SshCommand {
   @Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
   private String reason;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index d6fecab..c209249 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
@@ -47,6 +48,7 @@
 
 /** Create a new user account. **/
 @RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
+@CommandMetaData(name = "create-account", descr = "Create a new batch/role account")
 final class CreateAccountCommand extends SshCommand {
   @Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
   private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index da08395..660460a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.PerformCreateGroup;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -37,6 +38,7 @@
  * Optionally, puts an initial set of user in the newly created group.
  */
 @RequiresCapability(GlobalCapability.CREATE_GROUP)
+@CommandMetaData(name = "create-group", descr = "Create a new account group")
 final class CreateGroupCommand extends SshCommand {
   @Option(name = "--owner", aliases = {"-o"}, metaVar = "GROUP", usage = "owning group, if not specified the group will be self-owning")
   private AccountGroup.Id ownerGroupId;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 5e23fed..fc7a3c7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.project.CreateProjectArgs;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.SuggestParentCandidates;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -35,6 +36,7 @@
 
 /** Create a new project. **/
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
+@CommandMetaData(name = "create-project", descr = "Create a new project and associated Git repository")
 final class CreateProjectCommand extends SshCommand {
   @Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
   void setProjectNameFromOption(String name) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index dbd2c54..2f89fcb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -36,26 +36,27 @@
     // SlaveCommandModule.
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
-    command(gerrit, "ban-commit").to(BanCommitCommand.class);
-    command(gerrit, "flush-caches").to(FlushCaches.class);
-    command(gerrit, "ls-projects").to(ListProjectsCommand.class);
-    command(gerrit, "ls-groups").to(ListGroupsCommand.class);
-    command(gerrit, "ls-user-refs").to(LsUserRefs.class);
-    command(gerrit, "query").to(Query.class);
-    command(gerrit, "show-caches").to(ShowCaches.class);
-    command(gerrit, "show-connections").to(ShowConnections.class);
-    command(gerrit, "show-queue").to(ShowQueue.class);
-    command(gerrit, "stream-events").to(StreamEvents.class);
-    command(gerrit, "version").to(VersionCommand.class);
+    command(gerrit, BanCommitCommand.class);
+    command(gerrit, FlushCaches.class);
+    command(gerrit, ListProjectsCommand.class);
+    command(gerrit, ListGroupsCommand.class);
+    command(gerrit, LsUserRefs.class);
+    command(gerrit, Query.class);
+    command(gerrit, ShowCaches.class);
+    command(gerrit, ShowConnections.class);
+    command(gerrit, ShowQueue.class);
+    command(gerrit, StreamEvents.class);
+    command(gerrit, VersionCommand.class);
 
     command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
-    command(plugin, "ls").to(PluginLsCommand.class);
-    command(plugin, "enable").to(PluginEnableCommand.class);
-    command(plugin, "install").to(PluginInstallCommand.class);
-    command(plugin, "reload").to(PluginReloadCommand.class);
-    command(plugin, "remove").to(PluginRemoveCommand.class);
-    command(plugin, "add").to(Commands.key(plugin, "install"));
-    command(plugin, "rm").to(Commands.key(plugin, "remove"));
+
+    command(plugin, PluginLsCommand.class);
+    command(plugin, PluginEnableCommand.class);
+    command(plugin, PluginInstallCommand.class);
+    command(plugin, PluginReloadCommand.class);
+    command(plugin, PluginRemoveCommand.class);
+    alias(plugin, "add", PluginInstallCommand.class);
+    alias(plugin, "rm", PluginRemoveCommand.class);
 
     command(git).toProvider(new DispatchCommandProvider(git));
     command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index fa63041..13abdf4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -31,6 +32,7 @@
 
 /** Causes the caches to purge all entries and reload. */
 @RequiresCapability(GlobalCapability.FLUSH_CACHES)
+@CommandMetaData(name = "flush-caches", descr = "Flush some/all server caches from memory")
 final class FlushCaches extends CacheCommand {
   private static final String WEB_SESSIONS = "web_sessions";
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index 412ea70..dda9f52 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -16,10 +16,12 @@
 
 import com.google.gerrit.server.group.ListGroups;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
 
+@CommandMetaData(name = "ls-groups", descr = "List groups visible to the caller")
 public class ListGroupsCommand extends BaseCommand {
   @Inject
   private ListGroups impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index 13e3f17..244028c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -16,10 +16,12 @@
 
 import com.google.gerrit.server.project.ListProjects;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
 
+@CommandMetaData(name = "ls-projects", descr = "List projects visible to the caller")
 final class ListProjectsCommand extends BaseCommand {
   @Inject
   private ListProjects impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index bacc629..2be3d86 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -39,6 +40,7 @@
 import java.util.Map;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "ls-user-refs", descr = "List refs visible to a specific user")
 public class LsUserRefs extends SshCommand {
   @Inject
   private AccountResolver accountResolver;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index a3f5280..7838b45 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -27,21 +27,23 @@
     final CommandName gerrit = Commands.named("gerrit");
     final CommandName testSubmit = Commands.named(gerrit, "test-submit");
 
-    command(gerrit, "approve").to(ReviewCommand.class);
-    command(gerrit, "create-account").to(CreateAccountCommand.class);
-    command(gerrit, "create-group").to(CreateGroupCommand.class);
-    command(gerrit, "rename-group").to(RenameGroupCommand.class);
-    command(gerrit, "create-project").to(CreateProjectCommand.class);
-    command(gerrit, "gsql").to(AdminQueryShell.class);
-    command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
-    command(gerrit, "receive-pack").to(Receive.class);
-    command(gerrit, "set-project-parent").to(AdminSetParent.class);
-    command(gerrit, "review").to(ReviewCommand.class);
-    command(gerrit, "set-account").to(SetAccountCommand.class);
-    command(gerrit, "set-project").to(SetProjectCommand.class);
+    command(gerrit, CreateAccountCommand.class);
+    command(gerrit, CreateGroupCommand.class);
+    command(gerrit, RenameGroupCommand.class);
+    command(gerrit, CreateProjectCommand.class);
+    command(gerrit, AdminQueryShell.class);
+    command(gerrit, TestSubmitRule.class);
+    command(gerrit, SetReviewersCommand.class);
+    command(gerrit, Receive.class);
+    command(gerrit, AdminSetParent.class);
+    command(gerrit, ReviewCommand.class);
+    // deprecated alias to review command
+    alias(gerrit, "approve", ReviewCommand.class);
+    command(gerrit, SetAccountCommand.class);
+    command(gerrit, SetProjectCommand.class);
 
     command(gerrit, "test-submit").toProvider(new DispatchCommandProvider(testSubmit));
-    command(testSubmit, "rule").to(TestSubmitRule.class);
-    command(testSubmit, "type").to(TestSubmitType.class);
+    command(testSubmit, TestSubmitRule.class);
+    command(testSubmit, TestSubmitType.class);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
index 4df3aee..709e337 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginInstallException;
 import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -27,6 +28,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "enable", descr = "Enable plugins")
 final class PluginEnableCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin(s) to enable")
   List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 12722ec..fc036fe 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginInstallException;
 import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -34,6 +35,7 @@
 import java.net.URL;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "install", descr = "Install/Add a plugin")
 final class PluginInstallCommand extends SshCommand {
   @Option(name = "--name", aliases = {"-n"}, usage = "install under name")
   private String name;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index 6d7490f..ab6c978 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.ListPlugins;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
@@ -25,6 +26,7 @@
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "ls", descr = "List the installed plugins")
 final class PluginLsCommand extends BaseCommand {
   @Inject
   private ListPlugins impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
index d2429a9..85ade03 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.plugins.PluginInstallException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -27,6 +28,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "reload", descr = "Reload/Restart plugins")
 final class PluginReloadCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
   private List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
index 8baab77..96adb8fe 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -26,6 +27,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "remove", descr = "Disable plugins")
 final class PluginRemoveCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove")
   List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index 63680f8..2d17876 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.server.query.change.QueryProcessor;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -23,6 +24,7 @@
 
 import java.util.List;
 
+@CommandMetaData(name = "query", descr = "Query the change database")
 class Query extends SshCommand {
   @Inject
   private QueryProcessor processor;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index b4de75b..e0dd38f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.TooLargeObjectInPackException;
@@ -41,6 +42,7 @@
 import java.util.Set;
 
 /** Receives change upload over SSH using the Git receive-pack protocol. */
+@CommandMetaData(name = "receive-pack", descr = "Standard Git server side command for client side git push")
 final class Receive extends AbstractGitCommand {
   private static final Logger log = LoggerFactory.getLogger(Receive.class);
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index b9abc92..f3c1bb3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -18,12 +18,14 @@
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.server.account.PerformRenameGroup;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.Argument;
 
+@CommandMetaData(name = "rename-group", descr = "Rename an account group")
 public class RenameGroupCommand extends SshCommand {
   @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
   private String groupName;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 9f4fcb7..8e5f3ad 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -41,6 +41,7 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.OrmException;
@@ -60,6 +61,7 @@
 import java.util.List;
 import java.util.Set;
 
+@CommandMetaData(name = "review", descr = "Verify, approve and/or submit one or more patch sets")
 public class ReviewCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(ReviewCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 9940fc8..8b8e1d2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
@@ -46,6 +47,7 @@
 import java.util.List;
 
 /** Set a user's account settings. **/
+@CommandMetaData(name = "set-account", descr = "Change an account's settings")
 final class SetAccountCommand extends BaseCommand {
 
   @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index c2f1c9e..8c06c97d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -37,6 +38,7 @@
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "set-project", descr = "Change a project's settings")
 final class SetProjectCommand extends SshCommand {
   private static final Logger log = LoggerFactory
       .getLogger(SetProjectCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index f873824..311300e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -42,6 +43,7 @@
 import java.util.List;
 import java.util.Set;
 
+@CommandMetaData(name = "set-reviewers", descr = "Add or remove reviewers on a change")
 public class SetReviewersCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(SetReviewersCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index f2baddd..adf4482 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -50,6 +51,7 @@
 
 /** Show the current cache states. */
 @RequiresCapability(GlobalCapability.VIEW_CACHES)
+@CommandMetaData(name = "show-caches", descr = "Display current cache statistics")
 final class ShowCaches extends CacheCommand {
   private static volatile long serverStarted;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index a1a5b8f..1c3d828 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.gerrit.sshd.SshSession;
@@ -41,6 +42,7 @@
 
 /** Show the current SSH connections. */
 @RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
+@CommandMetaData(name = "show-connections", descr = "Display active client SSH connections")
 final class ShowConnections extends SshCommand {
   @Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
   private boolean numeric;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index f862484..fee5275 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -39,6 +40,7 @@
 
 /** Display the current work queue. */
 @AdminHighPriorityCommand
+@CommandMetaData(name = "show-queue", descr = "Display the background work queues, including replication")
 final class ShowQueue extends SshCommand {
   @Option(name = "-w", usage = "display without line width truncation")
   private boolean wide;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index ff6dea9..1b81a47 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.StreamCommandExecutor;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
@@ -32,6 +33,7 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.LinkedBlockingQueue;
 
+@CommandMetaData(name = "stream-events", descr = "Monitor events occurring in real time")
 final class StreamEvents extends BaseCommand {
   /** Maximum number of events that may be queued up for each connection. */
   private static final int MAX_EVENTS = 128;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
index dc4d767..b0729d9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.events.SubmitRecordAttribute;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gson.reflect.TypeToken;
 
 import com.googlecode.prolog_cafe.lang.ListTerm;
@@ -33,6 +34,7 @@
 import java.util.List;
 
 /** Command that allows testing of prolog submit-rules in a live instance. */
+@CommandMetaData(name = "rule", descr = "Test prolog submit rules")
 final class TestSubmitRule extends BaseTestSubmit {
 
   protected SubmitRuleEvaluator createEvaluator(PatchSet ps) throws Exception {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitType.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitType.java
index 354deca7..fb4a811 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitType.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitType.java
@@ -19,12 +19,14 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.sshd.CommandMetaData;
 
 import com.googlecode.prolog_cafe.lang.ListTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
 import java.util.List;
 
+@CommandMetaData(name = "type", descr = "Test prolog submit type")
 final class TestSubmitType extends BaseTestSubmit {
 
   @Override
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
index addbb84..2066cc2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 
+@CommandMetaData(name = "version", descr = "Display gerrit version")
 final class VersionCommand extends SshCommand {
+
   @Override
   protected void run() throws Failure {
     String v = Version.getVersion();