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