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