diff --git a/pom.xml b/pom.xml
index 29645e620b..b8aadaafff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
github-api
- 1.16
+ 1.17
GitHub API for Java
http://github-api.kohsuke.org/
GitHub API for Java
@@ -77,6 +77,12 @@
1.4
true
+
+ org.kohsuke.stapler
+ stapler-jetty
+ 1.1
+ test
+
diff --git a/src/main/java/org/kohsuke/github/ApiVersion.java b/src/main/java/org/kohsuke/github/ApiVersion.java
index 52ecedb14e..922dbe7aa1 100644
--- a/src/main/java/org/kohsuke/github/ApiVersion.java
+++ b/src/main/java/org/kohsuke/github/ApiVersion.java
@@ -6,12 +6,19 @@
* @author Kohsuke Kawaguchi
*/
enum ApiVersion {
- V2("https://github.com/api/v2/json"),
- V3("https://api.github.com");
+
+ V2("https://?/api/v2/json"),
+ V3("https://api.?");
- final String url;
+ final String templateUrl;
- ApiVersion(String url) {
- this.url = url;
+ ApiVersion(String templateUrl) {
+ this.templateUrl = templateUrl;
+ }
+
+ public String getApiVersionBaseUrl(String githubServer) {
+
+ return templateUrl.replaceFirst("\\?", githubServer);
+
}
}
diff --git a/src/main/java/org/kohsuke/github/GHCommitPointer.java b/src/main/java/org/kohsuke/github/GHCommitPointer.java
index 3e0a5d4ad0..c4d67f6410 100644
--- a/src/main/java/org/kohsuke/github/GHCommitPointer.java
+++ b/src/main/java/org/kohsuke/github/GHCommitPointer.java
@@ -31,7 +31,7 @@
public class GHCommitPointer {
private String ref, sha, label;
private GHUser user;
- private GHRepository repository;
+ private GHRepository repository/*V2*/,repo/*V3*/;
/**
* This points to the user who owns
@@ -45,11 +45,11 @@ public GHUser getUser() {
* The repository that contains the commit.
*/
public GHRepository getRepository() {
- return repository;
+ return repo!=null ? repo : repository;
}
/**
- * Named ref to the commit.
+ * Named ref to the commit. This appears to be a "short ref" that doesn't include "refs/heads/" portion.
*/
public String getRef() {
return ref;
@@ -68,4 +68,10 @@ public String getSha() {
public String getLabel() {
return label;
}
+
+ void wrapUp(GitHub root) {
+ if (user!=null) user.root = root;
+ if (repo!=null) repo.wrap(root);
+ if (repository!=null) repository.wrap(root);
+ }
}
diff --git a/src/main/java/org/kohsuke/github/GHEventInfo.java b/src/main/java/org/kohsuke/github/GHEventInfo.java
new file mode 100644
index 0000000000..fe9aada27f
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHEventInfo.java
@@ -0,0 +1,82 @@
+package org.kohsuke.github;
+
+import org.codehaus.jackson.node.ObjectNode;
+
+import java.io.IOException;
+import java.util.Date;
+
+/**
+ * Represents an event.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class GHEventInfo {
+ private GitHub root;
+
+ // we don't want to expose Jackson dependency to the user. This needs databinding
+ private ObjectNode payload;
+
+ private String created_at;
+ private String type;
+
+ // these are all shallow objects
+ private GHEventRepository repo;
+ private GHUser actor;
+ private GHOrganization org;
+
+ /**
+ * Inside the event JSON model, GitHub uses a slightly different format.
+ */
+ public static class GHEventRepository {
+ private int id;
+ private String url; // repository API URL
+ private String name; // owner/repo
+ }
+
+ public GHEvent getType() {
+ String t = type;
+ if (t.endsWith("Event")) t=t.substring(0,t.length()-5);
+ for (GHEvent e : GHEvent.values()) {
+ if (e.name().replace("_","").equalsIgnoreCase(t))
+ return e;
+ }
+ return null; // unknown event type
+ }
+
+ /*package*/ GHEventInfo wrapUp(GitHub root) {
+ this.root = root;
+ return this;
+ }
+
+ public Date getCreatedAt() {
+ return GitHub.parseDate(created_at);
+ }
+
+ /**
+ * Repository where the change was made.
+ */
+ public GHRepository getRepository() throws IOException {
+ return root.getRepository(repo.name);
+ }
+
+ public GHUser getActor() throws IOException {
+ return root.getUser(actor.getLogin());
+ }
+
+ public GHOrganization getOrganization() throws IOException {
+ return (org==null || org.getLogin()==null) ? null : root.getOrganization(org.getLogin());
+ }
+
+ /**
+ * Retrieves the payload.
+ *
+ * @param type
+ * Specify one of the {@link GHEventPayload} subtype that defines a type-safe access to the payload.
+ * This must match the {@linkplain #getType() event type}.
+ */
+ public T getPayload(Class type) throws IOException {
+ T v = GitHub.MAPPER.readValue(payload.traverse(), type);
+ v.wrapUp(root);
+ return v;
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GHEventPayload.java b/src/main/java/org/kohsuke/github/GHEventPayload.java
new file mode 100644
index 0000000000..efb3d0d3fe
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHEventPayload.java
@@ -0,0 +1,46 @@
+package org.kohsuke.github;
+
+import java.io.Reader;
+
+/**
+ * Base type for types used in databinding of the event payload.
+ *
+ * @see GitHub#parseEventPayload(Reader, Class)
+ * @see GHEventInfo#getPayload(Class)
+ */
+public abstract class GHEventPayload {
+ protected GitHub root;
+
+ /*package*/ GHEventPayload() {
+ }
+
+ /*package*/ void wrapUp(GitHub root) {
+ this.root = root;
+ }
+
+ public static class PullRequest extends GHEventPayload {
+ private String action;
+ private int number;
+ GHPullRequest pull_request;
+
+ public String getAction() {
+ return action;
+ }
+
+ public int getNumber() {
+ return number;
+ }
+
+ public GHPullRequest getPullRequest() {
+ pull_request.root = root;
+ return pull_request;
+ }
+
+ @Override
+ void wrapUp(GitHub root) {
+ super.wrapUp(root);
+ pull_request.wrapUp(root);
+ }
+ }
+
+}
diff --git a/src/main/java/org/kohsuke/github/GHPullRequest.java b/src/main/java/org/kohsuke/github/GHPullRequest.java
index 323b5874a7..1f86981e0f 100644
--- a/src/main/java/org/kohsuke/github/GHPullRequest.java
+++ b/src/main/java/org/kohsuke/github/GHPullRequest.java
@@ -25,7 +25,6 @@
import java.net.URL;
import java.util.Date;
-import java.util.Locale;
/**
* A pull request.
@@ -64,7 +63,7 @@ public GHCommitPointer getBase() {
}
/**
- * The change that should be pulled.
+ * The change that should be pulled. The tip of the commits to merge.
*/
public GHCommitPointer getHead() {
return head;
@@ -93,4 +92,19 @@ public URL getDiffUrl() {
public Date getClosedAt() {
return GitHub.parseDate(closed_at);
}
+
+ GHPullRequest wrapUp(GHRepository owner) {
+ this.owner = owner;
+ return wrapUp(owner.root);
+ }
+
+ GHPullRequest wrapUp(GitHub root) {
+ this.root = root;
+ if (owner!=null) owner.wrap(root);
+ if (issue_user!=null) issue_user.root=root;
+ if (user!=null) user.root=root;
+ if (base!=null) base.wrapUp(root);
+ if (head!=null) head.wrapUp(root);
+ return this;
+ }
}
diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java
index 81b584b4ef..20392d1192 100644
--- a/src/main/java/org/kohsuke/github/GHRepository.java
+++ b/src/main/java/org/kohsuke/github/GHRepository.java
@@ -78,12 +78,31 @@ public String getHomepage() {
}
/**
- * URL of this repository, like 'http://github.com/kohsuke/hudson'
+ * URL of this repository, like 'http://github.com/kohsuke/jenkins'
*/
public String getUrl() {
return html_url;
}
+ /**
+ * Gets the git:// URL to this repository, such as "git://github.com/kohsuke/jenkins.git"
+ * This URL is read-only.
+ */
+ public String getGitTransportUrl() {
+ return "git://github.com/"+getOwnerName()+"/"+name+".git";
+ }
+
+ /**
+ * Gets the HTTPS URL to this repository, such as "https://github.com/kohsuke/jenkins.git"
+ * This URL is read-only.
+ */
+ public String gitHttpTransportUrl() {
+ return "https://github.com/"+getOwnerName()+"/"+name+".git";
+ }
+
+ /**
+ * Short repository name without the owner. For example 'jenkins' in case of http://github.com/jenkinsci/jenkins
+ */
public String getName() {
return name;
}
@@ -290,14 +309,17 @@ public void renameTo(String newName) throws IOException {
* Retrieves a specified pull request.
*/
public GHPullRequest getPullRequest(int i) throws IOException {
- return root.retrieveWithAuth("/pulls/" + owner.login + '/' + name + "/" + i, JsonPullRequest.class).wrap(this);
+ return root.retrieveWithAuth3("/repos/" + owner.login + '/' + name + "/pulls/" + i, GHPullRequest.class).wrapUp(this);
}
/**
* Retrieves all the pull requests of a particular state.
*/
public List getPullRequests(GHIssueState state) throws IOException {
- return root.retrieveWithAuth("/pulls/"+owner.login+'/'+name+"/"+state.name().toLowerCase(Locale.ENGLISH),JsonPullRequests.class).wrap(this);
+ GHPullRequest[] r = root.retrieveWithAuth3("/repos/" + owner.login + '/' + name + "/pulls?state=" + state.name().toLowerCase(Locale.ENGLISH), GHPullRequest[].class);
+ for (GHPullRequest p : r)
+ p.wrapUp(this);
+ return new ArrayList(Arrays.asList(r));
}
/**
@@ -321,7 +343,7 @@ public GHHook getHook(int id) throws IOException {
* TODO: produce type-safe binding
*
* @param name
- * Type of the hook to be created.
+ * Type of the hook to be created. See https://api.github.com/hooks for possible names.
* @param config
* The configuration hash.
* @param events
diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java
index 2cc3624f28..8f3c472c58 100644
--- a/src/main/java/org/kohsuke/github/GitHub.java
+++ b/src/main/java/org/kohsuke/github/GitHub.java
@@ -23,14 +23,10 @@
*/
package org.kohsuke.github;
-import com.gargoylesoftware.htmlunit.WebClient;
-import com.gargoylesoftware.htmlunit.html.HtmlForm;
-import com.gargoylesoftware.htmlunit.html.HtmlPage;
-import org.apache.commons.io.IOUtils;
-import org.codehaus.jackson.map.DeserializationConfig.Feature;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.codehaus.jackson.map.introspect.VisibilityChecker.Std;
-import sun.misc.BASE64Encoder;
+import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.ANY;
+import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.NONE;
+import static org.kohsuke.github.ApiVersion.V2;
+import static org.kohsuke.github.ApiVersion.V3;
import java.io.File;
import java.io.FileInputStream;
@@ -39,18 +35,30 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
+import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
+import java.util.TimeZone;
+
+import org.apache.commons.io.IOUtils;
+import org.codehaus.jackson.map.DeserializationConfig.Feature;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.codehaus.jackson.map.introspect.VisibilityChecker.Std;
-import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.*;
-import static org.kohsuke.github.ApiVersion.*;
+import sun.misc.BASE64Encoder;
+
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.html.HtmlForm;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
/**
* Root of the GitHub API.
@@ -66,9 +74,16 @@ public class GitHub {
private final Map users = new HashMap();
private final Map orgs = new HashMap();
/*package*/ String oauthAccessToken;
+
+ private final String githubServer;
- private GitHub(String login, String apiToken, String password) {
- this.login = login;
+ private GitHub(String login, String apiToken, String password) {
+ this ("github.com", login, apiToken, password);
+ }
+
+ private GitHub(String githubServer, String login, String apiToken, String password) {
+ this.githubServer = githubServer;
+ this.login = login;
this.apiToken = apiToken;
this.password = password;
@@ -80,8 +95,9 @@ private GitHub(String login, String apiToken, String password) {
encodedAuthorization = null;
}
- private GitHub (String oauthAccessToken) throws IOException {
+ private GitHub (String githubServer, String oauthAccessToken) throws IOException {
+ this.githubServer = githubServer;
this.password = null;
this.encodedAuthorization = null;
@@ -115,7 +131,11 @@ public static GitHub connect(String login, String apiToken, String password) thr
}
public static GitHub connectUsingOAuth (String accessToken) throws IOException {
- return new GitHub(accessToken);
+ return connectUsingOAuth("github.com", accessToken);
+ }
+
+ public static GitHub connectUsingOAuth (String githubServer, String accessToken) throws IOException {
+ return new GitHub(githubServer, accessToken);
}
/**
* Connects to GitHub anonymously.
@@ -137,7 +157,7 @@ public static GitHub connectAnonymously() {
tailApiUrl = tailApiUrl + (tailApiUrl.indexOf('?')>=0 ?'&':'?') + "access_token=" + oauthAccessToken;
}
- return new URL(v.url+tailApiUrl);
+ return new URL(v.getApiVersionBaseUrl(githubServer)+tailApiUrl);
}
/*package*/ T retrieve(String tailApiUrl, Class type) throws IOException {
@@ -279,9 +299,42 @@ public GHOrganization getOrganization(String name) throws IOException {
return o;
}
+ /**
+ * Gets the repository object from 'user/reponame' string that GitHub calls as "repository name"
+ *
+ * @see GHRepository#getName()
+ */
+ public GHRepository getRepository(String name) throws IOException {
+ String[] tokens = name.split("/");
+ return getUser(tokens[0]).getRepository(tokens[1]);
+ }
+
public Map getMyOrganizations() throws IOException {
return retrieveWithAuth("/organizations",JsonOrganizations.class).wrap(this);
-
+ }
+
+ /**
+ * Public events visible to you. Equivalent of what's displayed on https://github.com/
+ */
+ public List getEvents() throws IOException {
+ // TODO: pagenation
+ GHEventInfo[] events = retrieve3("/events", GHEventInfo[].class);
+ for (GHEventInfo e : events)
+ e.wrapUp(this);
+ return Arrays.asList(events);
+ }
+
+ /**
+ * Parses the GitHub event object.
+ *
+ * This is primarily intended for receiving a POST HTTP call from a hook.
+ * Unfortunately, hook script payloads aren't self-descriptive, so you need
+ * to know the type of the payload you are expecting.
+ */
+ public T parseEventPayload(Reader r, Class type) throws IOException {
+ T t = MAPPER.readValue(r, type);
+ t.wrapUp(this);
+ return t;
}
/**
@@ -331,7 +384,9 @@ WebClient createWebClient() throws IOException {
/*package*/ static Date parseDate(String timestamp) {
for (String f : TIME_FORMATS) {
try {
- return new SimpleDateFormat(f).parse(timestamp);
+ SimpleDateFormat df = new SimpleDateFormat(f);
+ df.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return df.parse(timestamp);
} catch (ParseException e) {
// try next
}
diff --git a/src/main/java/org/kohsuke/github/JsonPullRequest.java b/src/main/java/org/kohsuke/github/JsonPullRequest.java
deleted file mode 100644
index 80cc367b2a..0000000000
--- a/src/main/java/org/kohsuke/github/JsonPullRequest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2010, Kohsuke Kawaguchi
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package org.kohsuke.github;
-
-/**
- * @author Kohsuke Kawaguchi
- */
-class JsonPullRequest {
- public GHPullRequest pull;
-
- public GHPullRequest wrap(GHRepository owner) {
- pull.owner = owner;
- pull.root = owner.root;
- return pull;
- }
-}
diff --git a/src/main/java/org/kohsuke/github/JsonPullRequests.java b/src/main/java/org/kohsuke/github/JsonPullRequests.java
deleted file mode 100644
index fd5e47eb58..0000000000
--- a/src/main/java/org/kohsuke/github/JsonPullRequests.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * The MIT License
- *
- * Copyright (c) 2010, Kohsuke Kawaguchi
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-package org.kohsuke.github;
-
-import java.util.List;
-
-/**
- * @author Kohsuke Kawaguchi
- */
-class JsonPullRequests {
- public List pulls;
-
- public List wrap(GHRepository owner) {
- for (GHPullRequest pull : pulls) {
- pull.owner = owner;
- pull.root = owner.root;
- }
- return pulls;
- }
-}
diff --git a/src/test/java/org/kohsuke/AppTest.java b/src/test/java/org/kohsuke/AppTest.java
index 7d8a1f8581..8e49623f93 100644
--- a/src/test/java/org/kohsuke/AppTest.java
+++ b/src/test/java/org/kohsuke/AppTest.java
@@ -1,7 +1,11 @@
package org.kohsuke;
import junit.framework.TestCase;
+import org.kohsuke.github.GHEvent;
+import org.kohsuke.github.GHEventInfo;
+import org.kohsuke.github.GHEventPayload;
import org.kohsuke.github.GHHook;
+import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHOrganization.Permission;
import org.kohsuke.github.GHRepository;
@@ -21,6 +25,13 @@ public void testCredentialValid() throws IOException {
assertFalse(GitHub.connect("totally","bogus").isCredentialValid());
}
+ public void testFetchPullRequest() throws Exception {
+ GitHub gh = GitHub.connect();
+ GHRepository r = gh.getOrganization("jenkinsci").getRepository("jenkins");
+ r.getPullRequest(1);
+ r.getPullRequests(GHIssueState.OPEN);
+ }
+
public void tryOrgFork() throws Exception {
GitHub gh = GitHub.connect();
gh.getUser("kohsuke").getRepository("rubywm").forkTo(gh.getOrganization("jenkinsci"));
@@ -56,6 +67,14 @@ public void tryHook() throws Exception {
public void testApp() throws IOException {
GitHub gitHub = GitHub.connect();
+ for (GHEventInfo ev : gitHub.getEvents()) {
+ System.out.println(ev);
+ if (ev.getType()==GHEvent.PULL_REQUEST) {
+ GHEventPayload.PullRequest pr = ev.getPayload(GHEventPayload.PullRequest.class);
+ System.out.println(pr.getNumber());
+ System.out.println(pr.getPullRequest());
+ }
+ }
// GHRepository r = gitHub.connect().getOrganization("jenkinsci").createRepository("kktest4", "Kohsuke's test", "http://kohsuke.org/", "Everyone", true);
// r.fork();
diff --git a/src/test/java/org/kohsuke/HookApp.java b/src/test/java/org/kohsuke/HookApp.java
new file mode 100644
index 0000000000..aac24ddf84
--- /dev/null
+++ b/src/test/java/org/kohsuke/HookApp.java
@@ -0,0 +1,32 @@
+package org.kohsuke;
+
+import org.kohsuke.github.GHEventPayload;
+import org.kohsuke.github.GitHub;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.jetty.JettyRunner;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * App to test the hook script. You need some internet-facing server that can forward the request to you
+ * (typically via SSH reverse port forwarding.)
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public class HookApp {
+ public static void main(String[] args) throws Exception {
+// GitHub.connect().getMyself().getRepository("sandbox").createWebHook(
+// new URL("http://173.203.118.45:18080/"), EnumSet.of(GHEvent.PULL_REQUEST));
+ JettyRunner jr = new JettyRunner(new HookApp());
+ jr.addHttpListener(8080);
+ jr.start();
+ }
+
+ public void doIndex(StaplerRequest req) throws IOException {
+ String str = req.getParameter("payload");
+ System.out.println(str);
+ GHEventPayload.PullRequest o = GitHub.connect().parseEventPayload(new StringReader(str),GHEventPayload.PullRequest.class);
+ System.out.println(o);
+ }
+}