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); + } +}