diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt index 780231cabc..5637e80cd5 100644 --- a/Documentation/cmd-index.txt +++ b/Documentation/cmd-index.txt @@ -60,6 +60,9 @@ link:cmd-ban-commit.html[gerrit ban-commit]:: link:cmd-ls-groups.html[gerrit ls-groups]:: List groups visible to the caller. +link:cmd-ls-members.html[gerrit ls-members]:: + List the membership of a group visible to the caller. + link:cmd-ls-projects.html[gerrit ls-projects]:: List projects visible to the caller. diff --git a/Documentation/cmd-ls-members.txt b/Documentation/cmd-ls-members.txt new file mode 100644 index 0000000000..9814ff2458 --- /dev/null +++ b/Documentation/cmd-ls-members.txt @@ -0,0 +1,64 @@ +gerrit ls-members +================ + +NAME +---- +gerrit ls-members - Show members of a given group + +SYNOPSIS +-------- +[verse] +'ssh' -p 'gerrit ls-members GROUPNAME' + [--recursive] + +DESCRIPTION +----------- +Displays the members of the given group, one per line, so long as the given +group is visible to the user. The users' id, username, full name and email are +shown tab-separated. + +ACCESS +------ +Any user who has configured an SSH key. + +SCRIPTING +--------- +This command is intended to be used in scripts. Output is either an error +message or a heading followed by zero or more lines, one for each member of the +group. If any field is not set, or if the field is the user's full name and the +name is empty, "n/a" is emitted as the field value. + +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 +------- +--recursive:: + If a member of the group is itself a group, the sub-group's + members are included in the list. Otherwise members of any sub-group + are not shown and no indication is given that a sub-group is present + +EXAMPLES +-------- + +List members of the Administrators group: +===== + $ ssh -p 29418 review.example.com gerrit ls-members Administrators + id username full name email + 100000 jim Jim Bob somebody@example.com + 100001 johnny John Smith n/a + 100002 mrnoname n/a someoneelse@example.com +===== + +List members of a non-existent group: +===== + $ ssh -p 29418 review.example.com gerrit ls-members BadlySpelledGroup + Group not found or not visible +===== + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index f17a741b58..54b10fdee3 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt @@ -31,7 +31,8 @@ Returns an account as an link:#account-info[AccountInfo] entity. { "_account_id": 1000096, "name": "John Doe", - "email": "john.doe@example.com" + "email": "john.doe@example.com", + "username": "john" } ---- @@ -410,6 +411,8 @@ Only set if detailed account information is requested. |`email` |optional| The email address the user prefers to be contacted through. + Only set if detailed account information is requested. +|`username` |optional|The username of the user. + +Only set if detailed account information is requested. |=========================== [[capability-info]] diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt index e800d569ee..4785350ec6 100644 --- a/Documentation/rest-api-groups.txt +++ b/Documentation/rest-api-groups.txt @@ -293,12 +293,14 @@ describes the group. { "_account_id": 1000097, "name": "Jane Roe", - "email": "jane.roe@example.com" + "email": "jane.roe@example.com", + "username": "jane" }, { "_account_id": 1000096, "name": "John Doe", "email": "john.doe@example.com" + "username": "john" } ], "includes": [] @@ -620,12 +622,14 @@ full name, preferred email and id. { "_account_id": 1000097, "name": "Jane Roe", - "email": "jane.roe@example.com" + "email": "jane.roe@example.com", + "username": "jane" }, { "_account_id": 1000096, "name": "John Doe", - "email": "john.doe@example.com" + "email": "john.doe@example.com", + "username": "john" } ] ---- @@ -657,17 +661,20 @@ are not visible to the calling user are ignored. { "_account_id": 1000097, "name": "Jane Roe", - "email": "jane.roe@example.com" + "email": "jane.roe@example.com", + "username": "jane" }, { "_account_id": 1000096, "name": "John Doe", - "email": "john.doe@example.com" + "email": "john.doe@example.com", + "username": "john" }, { "_account_id": 1000098, "name": "Richard Roe", - "email": "richard.roe@example.com" + "email": "richard.roe@example.com", + "username": "rroe" } ] ---- @@ -698,7 +705,8 @@ AccountInfo] entity is returned that describes the group member. { "_account_id": 1000096, "name": "John Doe", - "email": "john.doe@example.com" + "email": "john.doe@example.com", + "username": "john" } ---- @@ -728,7 +736,8 @@ AccountInfo] entity is returned that describes the group member. { "_account_id": 1000037, "name": "John Doe", - "email": "john.doe@example.com" + "email": "john.doe@example.com", + "username": "john" } ---- @@ -782,12 +791,14 @@ already a member of the group. { "_account_id": 1000057, "name": "Jane Roe", - "email": "jane.roe@example.com" + "email": "jane.roe@example.com", + "username": "jane" }, { "_account_id": 1000037, "name": "John Doe", - "email": "john.doe@example.com" + "email": "john.doe@example.com", + "username": "john" } ] ---- diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java index a296716334..2f95368caf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java @@ -112,12 +112,14 @@ public class AccountInfo { public Integer _account_id; public String name; public String email; + public String username; private void fill(Account account, boolean detailed) { name = account.getFullName(); if (detailed) { _account_id = account.getId().get(); email = account.getPreferredEmail(); + username = account.getUserName(); } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java index d32c632233..28f908cc9a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java @@ -49,7 +49,7 @@ public class ListMembers implements RestReadView { private boolean recursive; @Inject - ListMembers(GroupCache groupCache, + protected ListMembers(GroupCache groupCache, GroupDetailFactory.Factory groupDetailFactory, AccountInfo.Loader.Factory accountLoaderFactory) { this.groupCache = groupCache; @@ -63,8 +63,19 @@ public class ListMembers implements RestReadView { if (resource.toAccountGroup() == null) { throw new MethodNotAllowedException(); } + + return apply(resource.getGroupUUID()); + } + + public List apply(AccountGroup group) + throws MethodNotAllowedException, OrmException { + return apply(group.getGroupUUID()); + } + + public List apply(AccountGroup.UUID groupId) + throws MethodNotAllowedException, OrmException { final Map members = - getMembers(resource.getGroupUUID(), new HashSet()); + getMembers(groupId, new HashSet()); final List memberInfos = Lists.newArrayList(members.values()); Collections.sort(memberInfos, new Comparator() { @Override 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 35a4e141b8..521103b7af 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 @@ -39,6 +39,7 @@ public class DefaultCommandModule extends CommandModule { command(gerrit, BanCommitCommand.class); command(gerrit, FlushCaches.class); command(gerrit, ListProjectsCommand.class); + command(gerrit, ListMembersCommand.class); command(gerrit, ListGroupsCommand.class); command(gerrit, LsUserRefs.class); command(gerrit, Query.class); diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java new file mode 100644 index 0000000000..c5bda3c7f7 --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java @@ -0,0 +1,116 @@ +// 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.commands; + +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountInfo; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.account.GroupDetailFactory.Factory; +import com.google.gerrit.server.group.ListMembers; +import com.google.gerrit.server.ioutil.ColumnFormatter; +import com.google.gerrit.sshd.BaseCommand; +import com.google.gerrit.sshd.CommandMetaData; +import com.google.gwtorm.server.OrmException; + +import org.apache.sshd.server.Environment; +import org.kohsuke.args4j.Argument; + +import java.io.PrintWriter; +import java.util.List; + +import javax.inject.Inject; + +/** + * Implements a command that allows the user to see the members of a group. + */ +@CommandMetaData(name = "ls-members", descr = "Lists the members of a given group") +public class ListMembersCommand extends BaseCommand { + @Inject + ListMembersCommandImpl impl; + + @Override + public void start(Environment env) { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + parseCommandLine(impl); + final PrintWriter stdout = toPrintWriter(out); + try { + impl.display(stdout); + } finally { + stdout.flush(); + } + } + }); + } + + private static class ListMembersCommandImpl extends ListMembers { + @Argument(required = true, usage = "the name of the group", metaVar = "GROUPNAME") + private String name; + + private final GroupCache groupCache; + + @Inject + protected ListMembersCommandImpl(GroupCache groupCache, + Factory groupDetailFactory, + AccountInfo.Loader.Factory accountLoaderFactory, + AccountCache accountCache) { + super(groupCache, groupDetailFactory, accountLoaderFactory); + this.groupCache = groupCache; + } + + void display(PrintWriter writer) throws UnloggedFailure, OrmException { + AccountGroup group = groupCache.get(new AccountGroup.NameKey(name)); + String errorText = "Group not found or not visible\n"; + + if (group == null) { + writer.write(errorText); + writer.flush(); + return; + } + + try { + List members = apply(group.getGroupUUID()); + ColumnFormatter formatter = new ColumnFormatter(writer, '\t'); + formatter.addColumn("id"); + formatter.addColumn("username"); + formatter.addColumn("full name"); + formatter.addColumn("email"); + formatter.nextLine(); + for (AccountInfo member : members) { + if (member == null) { + continue; + } + + formatter.addColumn(member._id.toString()); + formatter.addColumn(Objects.firstNonNull(member.username, "n/a")); + formatter.addColumn(Objects.firstNonNull( + Strings.emptyToNull(member.name), "n/a")); + formatter.addColumn(Objects.firstNonNull(member.email, "n/a")); + formatter.nextLine(); + } + + formatter.finish(); + } catch (MethodNotAllowedException e) { + writer.write(errorText); + writer.flush(); + } + } + } +}