sshd: Add verbose output option to ls-groups command

The verbose mode enabled by the new option makes the ls-groups
command output a tab-separated table containing all available
information about each group (though not its members).

Change-Id: I8514a784c9e838d28edb62e5ca84fb4514d2928c
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
index 8589255..a50657b 100644
--- a/Documentation/cmd-ls-groups.txt
+++ b/Documentation/cmd-ls-groups.txt
@@ -30,6 +30,12 @@
 ---------
 This command is intended to be used in scripts.
 
+All non-printable characters (ASCII value 31 or less) are escaped
+according to the conventions used in languages like C, Python, and Perl,
+employing standard sequences like `\n` and `\t`, and `\xNN` for all
+others. In shell scripts, the `printf` command can be used to unescape
+the output.
+
 OPTIONS
 -------
 --project::
@@ -68,6 +74,16 @@
 `system`:: Any system defined and managed group.
 --
 
+--verbose::
+-v::
+	Enable verbose output with tab-separated columns for the
+	group name, UUID, description, type (`SYSTEM` or `INTERNAL`),
+	owner group name, owner group UUID and whether the group is
+	visible to all (`true` or `false`).
++
+If a group has been "orphaned", i.e. its owner group UUID refers to a
+nonexistent group, the owner group name field will read `n/a`.
+
 EXAMPLES
 --------
 
@@ -90,6 +106,23 @@
 	Registered Users
 =====
 
+Extract the UUID of the 'Administrators' group:
+
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-groups -v | awk '-F\t' '$1 == "Administrators" {print $2}'
+	ad463411db3eec4e1efb0d73f55183c1db2fd82a
+=====
+
+Extract and expand the multi-line description of the 'Administrators'
+group:
+
+=====
+	$ printf "$(ssh -p 29418 review.example.com gerrit ls-groups -v | awk '-F\t' '$1 == "Administrators" {print $3}')\n"
+	This is a
+	multi-line
+	description.
+=====
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
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 a729f43..aa439c6 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
@@ -20,9 +20,12 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.VisibleGroups;
+import com.google.gerrit.server.ioutil.ColumnFormatter;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
@@ -33,6 +36,9 @@
 
 public class ListGroupsCommand extends SshCommand {
   @Inject
+  private GroupCache groupCache;
+
+  @Inject
   private VisibleGroups.Factory visibleGroupsFactory;
 
   @Inject
@@ -52,6 +58,12 @@
       usage = "user for which the groups should be listed")
   private Account.Id user;
 
+  @Option(name = "--verbose", aliases = {"-v"},
+      usage = "verbose output format with tab-separated columns for the " +
+          "group name, UUID, description, type, owner group name, " +
+          "owner group UUID, and whether the group is visible to all")
+  private boolean verboseOutput;
+
   @Override
   protected void run() throws Failure {
     try {
@@ -70,9 +82,26 @@
       } else {
         groupList = visibleGroups.get();
       }
+
+      final ColumnFormatter formatter = new ColumnFormatter(stdout, '\t');
       for (final GroupDetail groupDetail : groupList.getGroups()) {
-        stdout.print(groupDetail.group.getName() + "\n");
+        final AccountGroup g = groupDetail.group;
+        formatter.addColumn(g.getName());
+        if (verboseOutput) {
+          formatter.addColumn(KeyUtil.decode(g.getGroupUUID().toString()));
+          formatter.addColumn(
+              g.getDescription() != null ? g.getDescription() : "");
+          formatter.addColumn(g.getType().toString());
+          final AccountGroup owningGroup =
+              groupCache.get(g.getOwnerGroupUUID());
+          formatter.addColumn(
+              owningGroup != null ? owningGroup.getName() : "n/a");
+          formatter.addColumn(KeyUtil.decode(g.getOwnerGroupUUID().toString()));
+          formatter.addColumn(Boolean.toString(g.isVisibleToAll()));
+        }
+        formatter.nextLine();
       }
+      formatter.finish();
     } catch (OrmException e) {
       throw die(e);
     } catch (NoSuchGroupException e) {