Support for GitHub Teams as Gerrit Groups

GitHub Organisations’s Teams are now supported as logical sub-group
of a GitHub Organisation.

As Gerrit does not support nested external Groups, the nesting of 
GitHub Teams into Organisations is obtained by pure naming and
membership:
- UUID and name under the GitHub parent organisation
- membership resolved at GitHub level and cached

Sample scenario:
User jdoe belongs to GitHub organisation MyOrg and teams Owners and Devs

Gerrit will show as jdoe groups under /#/settings/group-memberships
/github/MyOrg
/github/MyOrg/Owners
/github/MyOrg/Devs

Change-Id: I2734eed4a08ddfcdeffd5dd077efac19b690c5b1
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java
index af72030..4c22c91 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java
@@ -80,15 +80,33 @@
       log.debug("Listing user's organisations starting with '{}'",
           orgNamePrefix);
 
-      String orgNamePrefixLowercase = orgNamePrefix.toLowerCase();
-      Set<String> ghOrgs = ghOrganisationCache.getOrganisationsForCurrentUser();
+      String[] namePrefixParts = orgNamePrefix.toLowerCase().split("/");
+      String orgNamePrefixLowercase =
+          namePrefixParts.length > 0 ? namePrefixParts[0] : "";
+      String teamNameLowercase =
+          namePrefixParts.length > 1 ? namePrefixParts[1] : "";
+
+      Set<String> ghOrgs = ghOrganisationCache.getOrganizationsForCurrentUser();
       log.debug("Full list of user's organisations: {}", ghOrgs);
 
       Builder<GroupReference> orgGroups =
           new ImmutableSet.Builder<GroupReference>();
       for (String ghOrg : ghOrgs) {
         if (ghOrg.toLowerCase().startsWith(orgNamePrefixLowercase)) {
-          orgGroups.add(GitHubOrganisationGroup.groupReference(ghOrg));
+          GroupReference teamGroupRef =
+              GitHubOrganisationGroup.groupReference(ghOrg);
+
+          if ((orgNamePrefixLowercase.length() > 0 && orgNamePrefix
+              .endsWith("/")) || teamNameLowercase.length() > 0) {
+            for (String teamName : ghOrganisationCache.getTeamsForCurrentUser(ghOrg)) {
+              if (teamName.toLowerCase().startsWith(teamNameLowercase)) {
+                orgGroups.add(GitHubTeamGroup.groupReference(teamGroupRef,
+                    teamName));
+              }
+            }
+          } else {
+            orgGroups.add(teamGroupRef);
+          }
         }
       }
       return orgGroups.build();
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupMembership.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupMembership.java
index 26a12bc..c66c8e0 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupMembership.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupMembership.java
@@ -34,8 +34,7 @@
       @Assisted String username) {
     this.groups =
         new ImmutableSet.Builder<UUID>().addAll(
-            ghOrganisationCache.getOrganisationsGroupsForUsername(username))
-            .build();
+            ghOrganisationCache.getAllGroupsForUser(username)).build();
   }
 
   @Override
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationsCache.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationsCache.java
index 56bbe31..671ddc3 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationsCache.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationsCache.java
@@ -17,15 +17,20 @@
 import static java.util.concurrent.TimeUnit.MINUTES;
 
 import java.util.Collections;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
+import org.kohsuke.github.GHTeam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.cache.CacheModule;
@@ -42,30 +47,40 @@
 public class GitHubOrganisationsCache {
   private static final Logger log = LoggerFactory
       .getLogger(GitHubOrganisationsCache.class);
-  private static final String CACHE_NAME = "organisations";
+  private static final String ORGS_CACHE_NAME = "org-teams";
   protected static final long GROUPS_CACHE_TTL_MINS = 15;
 
-  public static class Loader extends CacheLoader<String, Set<String>> {
-    private static final Logger log = LoggerFactory.getLogger(Loader.class);
+  public static class OrganisationLoader extends
+      CacheLoader<String, Multimap<String,String>> {
+    private static final Logger log = LoggerFactory
+        .getLogger(OrganisationLoader.class);
     private final UserScopedProvider<GitHubLogin> ghLoginProvider;
 
     @Inject
-    public Loader(UserScopedProvider<GitHubLogin> ghLoginProvider) {
+    public OrganisationLoader(UserScopedProvider<GitHubLogin> ghLoginProvider) {
       this.ghLoginProvider = ghLoginProvider;
     }
 
     @Override
-    public Set<String> load(String username) throws Exception {
+    public Multimap<String, String> load(String username) throws Exception {
+      Multimap<String, String> orgsTeams = HashMultimap.create();
       GitHubLogin ghLogin = ghLoginProvider.get(username);
       if (ghLogin == null) {
         log.warn("Cannot login to GitHub on behalf of '{}'", username);
-        return Collections.emptySet();
+        return orgsTeams;
       }
 
-      log.debug("Getting list of organisations for user '{}'", username);
-      Set<String> myOrganisationsLogins = ghLogin.getMyOrganisationsLogins();
-      log.debug("GitHub user '{}' belongs to: {}", username, myOrganisationsLogins);
-      return myOrganisationsLogins;
+      log.debug("Getting list of organisations/teams for user '{}'", username);
+      Map<String, Set<GHTeam>> myOrganisationsLogins =
+          ghLogin.getHub().getMyTeams();
+      for (Entry<String, Set<GHTeam>> teamsOrg : myOrganisationsLogins
+          .entrySet()) {
+        for (GHTeam team : teamsOrg.getValue()) {
+          orgsTeams.put(teamsOrg.getKey(), team.getName());
+        }
+      }
+      log.debug("GitHub user '{}' belongs to: {}", username, orgsTeams);
+      return orgsTeams;
     }
   }
 
@@ -73,47 +88,63 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        cache(CACHE_NAME, String.class, new TypeLiteral<Set<String>>() {})
-            .expireAfterWrite(GROUPS_CACHE_TTL_MINS, MINUTES)
-            .loader(Loader.class);
+        cache(ORGS_CACHE_NAME, String.class, new TypeLiteral<Multimap<String,String>>() {})
+            .expireAfterWrite(GROUPS_CACHE_TTL_MINS, MINUTES).loader(
+                OrganisationLoader.class);
         bind(GitHubOrganisationsCache.class);
       }
     };
   }
 
-  private LoadingCache<String, Set<String>> byUsername;
+  private final LoadingCache<String, Multimap<String,String>> orgTeamsByUsername;
   private final Provider<IdentifiedUser> userProvider;
 
   @Inject
   public GitHubOrganisationsCache(
-      @Named(CACHE_NAME) LoadingCache<String, Set<String>> byUsername,
+      @Named(ORGS_CACHE_NAME) LoadingCache<String, Multimap<String,String>> byUsername,
       Provider<IdentifiedUser> userProvider) {
-    this.byUsername = byUsername;
+    this.orgTeamsByUsername = byUsername;
     this.userProvider = userProvider;
   }
 
-  public Set<String> getOrganisationsForUsername(String username)
-      throws ExecutionException {
-    return byUsername.get(username);
-  }
-  
-  public Set<String> getOrganisationsForCurrentUser()
-      throws ExecutionException {
-    return byUsername.get(userProvider.get().getUserName());
-  }
-
-  public Set<UUID> getOrganisationsGroupsForUsername(String username) {
+  public Set<String> getOrganizationsForUser(String username) {
     try {
-      Set<String> ghOrgsLogins = getOrganisationsForUsername(username);
-      log.debug("GitHub user '{}' belongs to: {}", username, ghOrgsLogins);
-      ImmutableSet.Builder<UUID> groupSet = new ImmutableSet.Builder<UUID>();
-      for (String ghOrg : ghOrgsLogins) {
-        groupSet.add(GitHubOrganisationGroup.uuid(ghOrg));
-      }
-      return groupSet.build();
+      return orgTeamsByUsername.get(username).keySet();
     } catch (ExecutionException e) {
       log.warn("Cannot get GitHub organisations for user '" + username + "'", e);
+      return Collections.emptySet();
     }
-    return Collections.emptySet();
+  }
+
+  public Set<String> getOrganizationsForCurrentUser() throws ExecutionException {
+    return orgTeamsByUsername.get(userProvider.get().getUserName()).keySet();
+  }
+
+  public Set<String> getTeamsForUser(String organizationName, String username) {
+    try {
+      return new ImmutableSet.Builder<String>().addAll(
+          orgTeamsByUsername.get(username).get(organizationName)).build();
+    } catch (ExecutionException e) {
+      log.warn("Cannot get Teams membership for organisation '" + organizationName
+          + "' and user '" + username + "'", e);
+      return Collections.emptySet();
+    }
+  }
+
+  public Set<String> getTeamsForCurrentUser(String organizationName) {
+    return getTeamsForUser(organizationName, userProvider.get().getUserName());
+  }
+
+  public Set<UUID> getAllGroupsForUser(String username) {
+    ImmutableSet.Builder<UUID> groupsBuilder = new ImmutableSet.Builder<>();
+      for (String org : getOrganizationsForUser(username)) {
+        groupsBuilder.add(GitHubOrganisationGroup.uuid(org));
+
+        for (String team : getTeamsForUser(org, username)) {
+          groupsBuilder.add(GitHubTeamGroup.uuid(
+              GitHubOrganisationGroup.uuid(org), team));
+        }
+      }
+      return groupsBuilder.build();
   }
 }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubTeamGroup.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubTeamGroup.java
new file mode 100644
index 0000000..345d2b3
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubTeamGroup.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2014 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.googlesource.gerrit.plugins.github.group;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.inject.assistedinject.Assisted;
+
+public class GitHubTeamGroup extends GitHubGroup {
+  public interface Factory {
+    GitHubTeamGroup get(@Assisted GitHubOrganisationGroup orgGroup,
+        @Assisted String teamName, @Nullable String teamUrl);
+  }
+
+  private final GitHubOrganisationGroup orgGroup;
+  private final String teamName;
+
+  public GitHubTeamGroup(@Assisted GitHubOrganisationGroup orgGroup,
+      @Assisted String teamName, @Nullable String teamUrl) {
+    super(uuid(orgGroup.uuid, teamName), teamUrl);
+    this.orgGroup = orgGroup;
+    this.teamName = teamName;
+  }
+
+  @Override
+  public String getName() {
+    return orgGroup.getName() + "/" + teamName;
+  }
+
+  public static UUID uuid(UUID orgUUID, String teamName) {
+    return new AccountGroup.UUID(orgUUID.get() + "/" + teamName);
+  }
+
+  public static GroupReference groupReference(GroupReference orgReference,
+      String teamName) {
+    return new GroupReference(uuid(orgReference.getUUID(), teamName),
+        orgReference.getName() + "/" + teamName);
+  }
+}