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