Initial version of the Pull-request plugin. Implemented so far the GitHub SSO and retrieval of the GitHub replication config. Change-Id: I5669f091bc50862adcbf000a12f8a0f528f36ce6
diff --git a/github-plugin/pom.xml b/github-plugin/pom.xml index 77c706e..b1bf14b 100644 --- a/github-plugin/pom.xml +++ b/github-plugin/pom.xml
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. --> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>github-parent</artifactId> @@ -41,12 +42,14 @@ <archive> <manifestEntries> <Gerrit-Module>com.googlesource.gerrit.plugins.github.Module</Gerrit-Module> + <Gerrit-HttpModule>com.googlesource.gerrit.plugins.github.HttpModule</Gerrit-HttpModule> <Gerrit-InitStep>com.googlesource.gerrit.plugins.github.InitGitHub</Gerrit-InitStep> <Implementation-Vendor>GerritForge</Implementation-Vendor> <Implementation-URL>http://www.gerritforge.com</Implementation-URL> - <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title> + <Implementation-Title>${Gerrit-ApiType} + ${project.artifactId}</Implementation-Title> <Implementation-Version>${project.version}</Implementation-Version> <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType> @@ -83,6 +86,13 @@ <version>4.8.1</version> <scope>test</scope> </dependency> + + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>github-oauth</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> <repositories>
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/HttpModule.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/HttpModule.java new file mode 100644 index 0000000..63b0bc3 --- /dev/null +++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/HttpModule.java
@@ -0,0 +1,20 @@ +package com.googlesource.gerrit.plugins.github; + +import org.apache.http.client.HttpClient; + +import com.google.inject.assistedinject.FactoryModuleBuilder; +import com.google.inject.servlet.ServletModule; +import com.googlesource.gerrit.plugins.github.oauth.GitHubHttpProvider; +import com.googlesource.gerrit.plugins.github.pullsync.PullRequestsServlet; +import com.googlesource.gerrit.plugins.github.replication.RemoteSiteUser; + +public class HttpModule extends ServletModule { + + @Override + protected void configureServlets() { + bind(HttpClient.class).toProvider(GitHubHttpProvider.class); + install(new FactoryModuleBuilder().build(RemoteSiteUser.Factory.class)); + + serve("/*").with(PullRequestsServlet.class); + } +}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/pullsync/PullRequestsServlet.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/pullsync/PullRequestsServlet.java new file mode 100644 index 0000000..3bb401d --- /dev/null +++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/pullsync/PullRequestsServlet.java
@@ -0,0 +1,56 @@ +package com.googlesource.gerrit.plugins.github.pullsync; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin; +import com.googlesource.gerrit.plugins.github.replication.Destination; +import com.googlesource.gerrit.plugins.github.replication.GitHubDestinations; + +@Singleton +public class PullRequestsServlet extends HttpServlet { + private static final long serialVersionUID = 3635343057427548273L; + private Provider<GitHubLogin> loginProvider; + private GitHubDestinations destinations; + + @Inject + public PullRequestsServlet(Provider<GitHubLogin> loginProvider, + GitHubDestinations destinations) { + this.loginProvider = loginProvider; + this.destinations = destinations; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + PrintWriter out = null; + try { + GitHubLogin hubLogin = loginProvider.get(); + if (!hubLogin.isLoggedIn()) { + if (!hubLogin.login(req, resp)) { + return; + } + } + out = resp.getWriter(); + + out.println("<html><body><pre>"); + for (Destination dest : destinations.getDestinations()) { + out.println(dest.getRemote().getURIs()); + }; + out.println("</pre></body></html>"); + } finally { + if (out != null) { + out.close(); + } + } + } +}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/Destination.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/Destination.java new file mode 100644 index 0000000..334d180 --- /dev/null +++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/Destination.java
@@ -0,0 +1,156 @@ +// Copyright (C) 2009 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.replication; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.gerrit.common.data.GroupReference; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.PluginUser; +import com.google.gerrit.server.account.GroupBackend; +import com.google.gerrit.server.account.GroupBackends; +import com.google.gerrit.server.account.ListGroupMembership; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.gerrit.server.project.ProjectControl; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Injector; + +public class Destination { + private final RemoteConfig remote; + private final ProjectControl.Factory projectControlFactory; + private final GitRepositoryManager gitManager; + private final String remoteNameStyle; + private final CurrentUser remoteUser; + + Destination(final Injector injector, final RemoteConfig rc, final Config cfg, + final SchemaFactory<ReviewDb> db, + final RemoteSiteUser.Factory replicationUserFactory, + final PluginUser pluginUser, + final GitRepositoryManager gitRepositoryManager, + final GroupBackend groupBackend) { + remote = rc; + gitManager = gitRepositoryManager; + + remoteNameStyle = + Objects.firstNonNull( + cfg.getString("remote", rc.getName(), "remoteNameStyle"), "slash"); + + String[] authGroupNames = + cfg.getStringList("remote", rc.getName(), "authGroup"); + if (authGroupNames.length > 0) { + ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder(); + for (String name : authGroupNames) { + GroupReference g = + GroupBackends.findExactSuggestion(groupBackend, name); + if (g != null) { + builder.add(g.getUUID()); + } else { + GitHubDestinations.log.warn(String.format( + "Group \"%s\" not recognized, removing from authGroup", name)); + } + } + remoteUser = + replicationUserFactory + .create(new ListGroupMembership(builder.build())); + } else { + remoteUser = pluginUser; + } + + projectControlFactory = injector.getInstance(ProjectControl.Factory.class); + } + + ProjectControl controlFor(Project.NameKey project) + throws NoSuchProjectException { + return projectControlFactory.controlFor(project); + } + + List<URIish> getURIs(Project.NameKey project, String urlMatch) { + List<URIish> r = Lists.newArrayListWithCapacity(remote.getURIs().size()); + for (URIish uri : remote.getURIs()) { + if (matches(uri, urlMatch)) { + String name = project.get(); + if (needsUrlEncoding(uri)) { + name = encode(name); + } + if (remoteNameStyle.equals("dash")) { + name = name.replace("/", "-"); + } else if (remoteNameStyle.equals("underscore")) { + name = name.replace("/", "_"); + } else if (!remoteNameStyle.equals("slash")) { + GitHubDestinations.log.debug(String.format( + "Unknown remoteNameStyle: %s, falling back to slash", + remoteNameStyle)); + } + String replacedPath = + GitHubDestinations.replaceName(uri.getPath(), name); + if (replacedPath != null) { + uri = uri.setPath(replacedPath); + r.add(uri); + } + } + } + return r; + } + + static boolean needsUrlEncoding(URIish uri) { + return "http".equalsIgnoreCase(uri.getScheme()) + || "https".equalsIgnoreCase(uri.getScheme()) + || "amazon-s3".equalsIgnoreCase(uri.getScheme()); + } + + static String encode(String str) { + try { + // Some cleanup is required. The '/' character is always encoded as %2F + // however remote servers will expect it to be not encoded as part of the + // path used to the repository. Space is incorrectly encoded as '+' for + // this + // context. In the path part of a URI space should be %20, but in form + // data + // space is '+'. Our cleanup replace fixes these two issues. + return URLEncoder.encode(str, "UTF-8").replaceAll("%2[fF]", "/") + .replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private static boolean matches(URIish uri, String urlMatch) { + if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) { + return true; + } + return uri.toString().contains(urlMatch); + } + + public RemoteConfig getRemote() { + return remote; + } + + public CurrentUser getRemoteUser() { + return remoteUser; + } +}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java new file mode 100644 index 0000000..45d277c --- /dev/null +++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/GitHubDestinations.java
@@ -0,0 +1,153 @@ +// Copyright (C) 2009 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.replication; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.PluginUser; +import com.google.gerrit.server.account.GroupBackend; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; +import com.google.inject.Injector; + +/** Manages automatic replication to remote repositories. */ +public class GitHubDestinations { + private static final String GITHUB_DESTINATION = "github"; + static final Logger log = LoggerFactory.getLogger(GitHubDestinations.class); + + static String replaceName(String in, String name) { + String key = "${name}"; + int n = in.indexOf(key); + if (0 <= n) { + return in.substring(0, n) + name + in.substring(n + key.length()); + } + return null; + } + + private final Injector injector; + private final List<Destination> configs; + + + private final SchemaFactory<ReviewDb> database; + private final RemoteSiteUser.Factory replicationUserFactory; + private final PluginUser pluginUser; + private final GitRepositoryManager gitRepositoryManager; + private final GroupBackend groupBackend; + boolean replicateAllOnPluginStart; + + @Inject + GitHubDestinations(final Injector i, final SitePaths site, + final RemoteSiteUser.Factory ruf, final SchemaFactory<ReviewDb> db, + final GitRepositoryManager grm, final GroupBackend gb, final PluginUser pu) + throws ConfigInvalidException, IOException { + injector = i; + database = db; + pluginUser = pu; + replicationUserFactory = ruf; + gitRepositoryManager = grm; + groupBackend = gb; + configs = getDestinations(new File(site.etc_dir, "replication.config")); + } + + private List<Destination> getDestinations(File cfgPath) + throws ConfigInvalidException, IOException { + FileBasedConfig cfg = new FileBasedConfig(cfgPath, FS.DETECTED); + if (!cfg.getFile().exists() || cfg.getFile().length() == 0) { + return Collections.emptyList(); + } + + try { + cfg.load(); + } catch (ConfigInvalidException e) { + throw new ConfigInvalidException(String.format( + "Config file %s is invalid: %s", cfg.getFile(), e.getMessage()), e); + } catch (IOException e) { + throw new IOException(String.format("Cannot read %s: %s", cfg.getFile(), + e.getMessage()), e); + } + + ImmutableList.Builder<Destination> dest = ImmutableList.builder(); + for (RemoteConfig c : allRemotes(cfg)) { + if (c.getURIs().isEmpty()) { + continue; + } + + for (URIish u : c.getURIs()) { + if (u.getPath() == null || !u.getPath().contains("${name}")) { + throw new ConfigInvalidException(String.format( + "remote.%s.url \"%s\" lacks ${name} placeholder in %s", + c.getName(), u, cfg.getFile())); + } + } + + // If destination for push is not set assume equal to source. + for (RefSpec ref : c.getPushRefSpecs()) { + if (ref.getDestination() == null) { + ref.setDestination(ref.getSource()); + } + } + + if (c.getPushRefSpecs().isEmpty()) { + c.addPushRefSpec(new RefSpec().setSourceDestination("refs/*", "refs/*") + .setForceUpdate(true)); + } + + dest.add(new Destination(injector, c, cfg, database, + replicationUserFactory, pluginUser, gitRepositoryManager, + groupBackend)); + } + return dest.build(); + } + + private static List<RemoteConfig> allRemotes(FileBasedConfig cfg) + throws ConfigInvalidException { + Set<String> names = cfg.getSubsections("remote"); + List<RemoteConfig> result = Lists.newArrayListWithCapacity(names.size()); + for (String name : names) { + try { + if (name.equalsIgnoreCase(GITHUB_DESTINATION)) { + result.add(new RemoteConfig(cfg, name)); + } + } catch (URISyntaxException e) { + throw new ConfigInvalidException(String.format( + "remote %s has invalid URL in %s", name, cfg.getFile())); + } + } + return result; + } + + public List<Destination> getDestinations() { + return configs; + } +}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/RemoteSiteUser.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/RemoteSiteUser.java new file mode 100644 index 0000000..34b5116 --- /dev/null +++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/replication/RemoteSiteUser.java
@@ -0,0 +1,57 @@ +// Copyright (C) 2009 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.replication; + +import com.google.gerrit.reviewdb.client.AccountProjectWatch; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.CapabilityControl; +import com.google.gerrit.server.account.GroupMembership; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +public class RemoteSiteUser extends CurrentUser { + public interface Factory { + RemoteSiteUser create(@Assisted GroupMembership authGroups); + } + + private final GroupMembership effectiveGroups; + + @Inject + RemoteSiteUser(CapabilityControl.Factory capabilityControlFactory, + @Assisted GroupMembership authGroups) { + super(capabilityControlFactory); + effectiveGroups = authGroups; + } + + @Override + public GroupMembership getEffectiveGroups() { + return effectiveGroups; + } + + @Override + public Set<Change.Id> getStarredChanges() { + return Collections.emptySet(); + } + + @Override + public Collection<AccountProjectWatch> getNotificationFilters() { + return Collections.emptySet(); + } +}