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