diff --git a/pom.xml b/pom.xml index 343fee323e..29645e620b 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.15 + 1.16 GitHub API for Java http://github-api.kohsuke.org/ GitHub API for Java @@ -25,6 +25,23 @@ + + + + com.infradna.tool + bridge-method-injector + 1.2 + + + + process + + + + + + + org.jvnet.hudson @@ -54,6 +71,12 @@ commons-io 1.4 + + com.infradna.tool + bridge-method-annotation + 1.4 + true + diff --git a/src/main/java/org/kohsuke/github/GHEvent.java b/src/main/java/org/kohsuke/github/GHEvent.java new file mode 100644 index 0000000000..8c255988b3 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHEvent.java @@ -0,0 +1,28 @@ +package org.kohsuke.github; + +/** + * Hook event type. + * + * See http://developer.github.com/v3/events/types/ + * + * @author Kohsuke Kawaguchi + */ +public enum GHEvent { + COMMIT_COMMENT, + CREATE, + DELETE, + DOWNLOAD, + FOLLOW, + FORK, + FORK_APPLY, + GIST, + GOLLUM, + ISSUE_COMMENT, + ISSUES, + MEMBER, + PUBLIC, + PULL_REQUEST, + PUSH, + TEAM_ADD, + WATCH +} diff --git a/src/main/java/org/kohsuke/github/GHHook.java b/src/main/java/org/kohsuke/github/GHHook.java new file mode 100644 index 0000000000..b539cc1dd7 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHHook.java @@ -0,0 +1,60 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * @author Kohsuke Kawaguchi + */ +public final class GHHook { + /** + * Repository that the hook belongs to. + */ + /*package*/ transient GHRepository repository; + + String created_at, updated_at, name; + List events; + boolean active; + Map config; + int id; + + /*package*/ GHHook wrap(GHRepository owner) { + this.repository = owner; + return this; + } + + public String getName() { + return name; + } + + public EnumSet getEvents() { + EnumSet s = EnumSet.noneOf(GHEvent.class); + for (String e : events) + Enum.valueOf(GHEvent.class,e.toUpperCase(Locale.ENGLISH)); + return s; + } + + public boolean isActive() { + return active; + } + + public Map getConfig() { + return Collections.unmodifiableMap(config); + } + + public int getId() { + return id; + } + + /** + * Deletes this hook. + */ + public void delete() throws IOException { + new Poster(repository.root,ApiVersion.V3).withCredential() + .to(String.format("/repos/%s/%s/hooks/%d",repository.getOwnerName(),repository.getName(),id),null,"DELETE"); + } +} diff --git a/src/main/java/org/kohsuke/github/GHOrganization.java b/src/main/java/org/kohsuke/github/GHOrganization.java index 96bf191c36..92fafaf319 100644 --- a/src/main/java/org/kohsuke/github/GHOrganization.java +++ b/src/main/java/org/kohsuke/github/GHOrganization.java @@ -2,9 +2,9 @@ import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.HtmlAnchor; -import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.AbstractList; import java.util.ArrayList; @@ -13,7 +13,7 @@ import java.util.List; import java.util.Map; -import static org.kohsuke.github.ApiVersion.V3; +import static org.kohsuke.github.ApiVersion.*; /** * @author Kohsuke Kawaguchi @@ -43,6 +43,15 @@ public Map getTeams() throws IOException { return root.retrieveWithAuth("/organizations/"+login+"/teams",JsonTeams.class).toMap(this); } + @Override + public GHRepository getRepository(String name) throws IOException { + try { + return root.retrieveWithAuth3("/repos/" + login + '/' + name, GHRepository.class).wrap(root); + } catch (FileNotFoundException e) { + return null; + } + } + /** * Publicizes the membership. */ diff --git a/src/main/java/org/kohsuke/github/GHPerson.java b/src/main/java/org/kohsuke/github/GHPerson.java index 393c342e35..f58ff4e57a 100644 --- a/src/main/java/org/kohsuke/github/GHPerson.java +++ b/src/main/java/org/kohsuke/github/GHPerson.java @@ -14,9 +14,17 @@ public abstract class GHPerson { /*package almost final*/ GitHub root; - protected String gravatar_id,login; + // common + protected String login,location,blog,email,name,created_at,company; + protected int id; + protected String gravatar_id; // appears in V3 as well but presumably subsumed by avatar_url? - protected int public_gist_count,public_repo_count,following_count,id; + // V2 + protected int public_gist_count,public_repo_count,followers_count,following_count; + + // V3 + protected String avatar_url,html_url; + protected int followers,following,public_repos,public_gists; /** * Gets the repositories this user owns. @@ -24,9 +32,12 @@ public abstract class GHPerson { public synchronized Map getRepositories() throws IOException { Map repositories = new TreeMap(); for (int i=1; ; i++) { - Map map = root.retrieve3("/user/" + login + "/repos?per_page=100&page=" + i, JsonRepositories.class).wrap(root); - repositories.putAll(map); - if (map.isEmpty()) break; + GHRepository[] array = root.retrieve3("/users/" + login + "/repos?per_page=100&page=" + i, GHRepository[].class); + for (GHRepository r : array) { + r.root = root; + repositories.put(r.getName(),r); + } + if (array.length==0) break; } return Collections.unmodifiableMap(repositories); @@ -52,7 +63,73 @@ public String getGravatarId() { return gravatar_id; } + /** + * Gets the login ID of this user, like 'kohsuke' + */ public String getLogin() { return login; } + + /** + * Gets the human-readable name of the user, like "Kohsuke Kawaguchi" + */ + public String getName() { + return name; + } + + /** + * Gets the company name of this user, like "Sun Microsystems, Inc." + */ + public String getCompany() { + return company; + } + + /** + * Gets the location of this user, like "Santa Clara, California" + */ + public String getLocation() { + return location; + } + + public String getCreatedAt() { + return created_at; + } + + /** + * Gets the blog URL of this user. + */ + public String getBlog() { + return blog; + } + + /** + * Gets the e-mail address of the user. + */ + public String getEmail() { + return email; + } + + public int getPublicGistCount() { + return Math.max(public_gist_count,public_gists); + } + + public int getPublicRepoCount() { + return Math.max(public_repo_count,public_repos); + } + + public int getFollowingCount() { + return Math.max(following_count,following); + } + + /** + * What appears to be a GitHub internal unique number that identifies this user. + */ + public int getId() { + return id; + } + + public int getFollowersCount() { + return Math.max(followers_count,followers); + } + } diff --git a/src/main/java/org/kohsuke/github/GHPersonSet.java b/src/main/java/org/kohsuke/github/GHPersonSet.java new file mode 100644 index 0000000000..5b98f036d4 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHPersonSet.java @@ -0,0 +1,36 @@ +package org.kohsuke.github; + +import java.util.Collection; +import java.util.HashSet; + +/** + * Set of {@link GHPerson} with helper lookup methods. + * + * @author Kohsuke Kawaguchi + */ +public final class GHPersonSet extends HashSet { + public GHPersonSet() { + } + + public GHPersonSet(Collection c) { + super(c); + } + + public GHPersonSet(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + } + + public GHPersonSet(int initialCapacity) { + super(initialCapacity); + } + + /** + * Finds the item by its login. + */ + public T byLogin(String login) { + for (T t : this) + if (t.getLogin().equals(login)) + return t; + return null; + } +} diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 44abaca1c3..81b584b4ef 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -30,14 +30,15 @@ import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlInput; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import java.io.IOException; -import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.net.URL; import java.util.AbstractSet; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -45,6 +46,7 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import static java.util.Arrays.*; @@ -143,11 +145,12 @@ public Date getCreatedAt() { * Gets the collaborators on this repository. * This set always appear to include the owner. */ - public Set getCollaborators() throws IOException { - Set r = new HashSet(); + @WithBridgeMethods(Set.class) + public GHPersonSet getCollaborators() throws IOException { + GHPersonSet r = new GHPersonSet(); for (String u : root.retrieve("/repos/show/"+owner.login+"/"+name+"/collaborators",JsonCollaborators.class).collaborators) r.add(root.getUser(u)); - return Collections.unmodifiableSet(r); + return r; } /** @@ -297,6 +300,58 @@ public List getPullRequests(GHIssueState state) throws IOExceptio return root.retrieveWithAuth("/pulls/"+owner.login+'/'+name+"/"+state.name().toLowerCase(Locale.ENGLISH),JsonPullRequests.class).wrap(this); } + /** + * Retrieves the currently configured hooks. + */ + public List getHooks() throws IOException { + List list = new ArrayList(Arrays.asList( + root.retrieveWithAuth3(String.format("/repos/%s/%s/hooks",owner.login,name),GHHook[].class))); + for (GHHook h : list) + h.wrap(this); + return list; + } + + public GHHook getHook(int id) throws IOException { + return root.retrieveWithAuth3(String.format("/repos/%s/%s/hooks/%d",owner.login,name,id),GHHook.class).wrap(this); + } + + /** + * + * See https://api.github.com/hooks for possible names and their configuration scheme. + * TODO: produce type-safe binding + * + * @param name + * Type of the hook to be created. + * @param config + * The configuration hash. + * @param events + * Can be null. Types of events to hook into. + */ + public GHHook createHook(String name, Map config, Collection events, boolean active) throws IOException { + List ea = null; + if (events!=null) { + ea = new ArrayList(); + for (GHEvent e : events) + ea.add(e.name().toLowerCase(Locale.ENGLISH)); + } + + return new Poster(root,ApiVersion.V3) + .withCredential() + .with("name",name) + .with("active", active) + ._with("config", config) + ._with("events",ea) + .to(String.format("/repos/%s/%s/hooks",owner.login,this.name),GHHook.class).wrap(this); + } + + public GHHook createWebHook(URL url, Collection events) throws IOException { + return createHook("web",Collections.singletonMap("url",url.toExternalForm()),events,true); + } + + public GHHook createWebHook(URL url) throws IOException { + return createWebHook(url,null); + } + // this is no different from getPullRequests(OPEN) // /** // * Retrieves all the pull requests. @@ -313,6 +368,9 @@ private void verifyMine() throws IOException { /** * Returns a set that represents the post-commit hook URLs. * The returned set is live, and changes made to them are reflected to GitHub. + * + * @deprecated + * Use {@link #getHooks()} and {@link #createHook(String, Map, Collection, boolean)} */ public Set getPostCommitHooks() { return postCommitHooks; @@ -324,15 +382,11 @@ public Set getPostCommitHooks() { private final Set postCommitHooks = new AbstractSet() { private List getPostCommitHooks() { try { - verifyMine(); - - HtmlForm f = getForm(); - List r = new ArrayList(); - for (HtmlInput i : f.getInputsByName("urls[]")) { - String v = i.getValueAttribute(); - if (v.length()==0) continue; - r.add(new URL(v)); + for (GHHook h : getHooks()) { + if (h.getName().equals("web")) { + r.add(new URL(h.getConfig().get("url"))); + } } return r; } catch (IOException e) { @@ -353,22 +407,7 @@ public int size() { @Override public boolean add(URL url) { try { - String u = url.toExternalForm(); - - verifyMine(); - - HtmlForm f = getForm(); - - List controls = f.getInputsByName("urls[]"); - for (HtmlInput i : controls) { - String v = i.getValueAttribute(); - if (v.length()==0) continue; - if (v.equals(u)) - return false; // already there - } - - controls.get(controls.size()-1).setValueAttribute(u); - f.submit(null); + createWebHook(url); return true; } catch (IOException e) { throw new GHException("Failed to update post-commit hooks",e); @@ -376,37 +415,20 @@ public boolean add(URL url) { } @Override - public boolean remove(Object o) { + public boolean remove(Object url) { try { - String u = ((URL)o).toExternalForm(); - - verifyMine(); - - HtmlForm f = getForm(); - - List controls = f.getInputsByName("urls[]"); - for (HtmlInput i : controls) { - String v = i.getValueAttribute(); - if (v.length()==0) continue; - if (v.equals(u)) { - i.setValueAttribute(""); - f.submit(null); + String _url = ((URL)url).toExternalForm(); + for (GHHook h : getHooks()) { + if (h.getName().equals("web") && h.getConfig().get("url").equals(_url)) { + h.delete(); return true; } } - return false; } catch (IOException e) { throw new GHException("Failed to update post-commit hooks",e); } } - - private HtmlForm getForm() throws IOException { - WebClient wc = root.createWebClient(); - HtmlPage pg = (HtmlPage)wc.getPage(getUrl()+"/admin"); - HtmlForm f = (HtmlForm) pg.getElementById("new_service"); - return f; - } }; /*package*/ GHRepository wrap(GitHub root) { diff --git a/src/main/java/org/kohsuke/github/GHUser.java b/src/main/java/org/kohsuke/github/GHUser.java index 178ea0e68b..fbf36c0e6d 100644 --- a/src/main/java/org/kohsuke/github/GHUser.java +++ b/src/main/java/org/kohsuke/github/GHUser.java @@ -23,7 +23,12 @@ */ package org.kohsuke.github; +import com.infradna.tool.bridge_method_injector.BridgeMethodsAdded; +import com.infradna.tool.bridge_method_injector.WithBridgeMethods; + import java.io.IOException; +import java.util.HashSet; +import java.util.Map; import java.util.Set; /** @@ -32,77 +37,6 @@ * @author Kohsuke Kawaguchi */ public class GHUser extends GHPerson { - private String name,company,location,created_at,blog,email; - private int followers_count; - - /** - * Gets the human-readable name of the user, like "Kohsuke Kawaguchi" - */ - public String getName() { - return name; - } - - /** - * Gets the company name of this user, like "Sun Microsystems, Inc." - */ - public String getCompany() { - return company; - } - - /** - * Gets the location of this user, like "Santa Clara, California" - */ - public String getLocation() { - return location; - } - - public String getCreatedAt() { - return created_at; - } - - /** - * Gets the blog URL of this user. - */ - public String getBlog() { - return blog; - } - - /** - * Gets the login ID of this user, like 'kohsuke' - */ - public String getLogin() { - return login; - } - - /** - * Gets the e-mail address of the user. - */ - public String getEmail() { - return email; - } - - public int getPublicGistCount() { - return public_gist_count; - } - - public int getPublicRepoCount() { - return public_repo_count; - } - - public int getFollowingCount() { - return following_count; - } - - /** - * What appears to be a GitHub internal unique number that identifies this user. - */ - public int getId() { - return id; - } - - public int getFollowersCount() { - return followers_count; - } /** * Follow this user. @@ -121,17 +55,33 @@ public void unfollow() throws IOException { /** * Lists the users that this user is following */ - public Set getFollows() throws IOException { + @WithBridgeMethods(Set.class) + public GHPersonSet getFollows() throws IOException { return root.retrieve("/user/show/"+login+"/following",JsonUsers.class).toSet(root); } /** * Lists the users who are following this user. */ - public Set getFollowers() throws IOException { + @WithBridgeMethods(Set.class) + public GHPersonSet getFollowers() throws IOException { return root.retrieve("/user/show/"+login+"/followers",JsonUsers.class).toSet(root); } + /** + * Gets the organization that this user belongs to publicly. + */ + @WithBridgeMethods(Set.class) + public GHPersonSet getOrganizations() throws IOException { + GHPersonSet orgs = new GHPersonSet(); + Set names = new HashSet(); + for (GHOrganization o : root.retrieve3("/users/"+login+"/orgs",GHOrganization[].class)) { + if (names.add(o.getLogin())) // I've seen some duplicates in the data + orgs.add(root.getOrganization(o.getLogin())); + } + return orgs; + } + @Override public String toString() { return "User:"+login; diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 118f5a538b..2cc3624f28 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -65,7 +65,7 @@ public class GitHub { private final Map users = new HashMap(); private final Map orgs = new HashMap(); - private String oauthAccessToken; + /*package*/ String oauthAccessToken; private GitHub(String login, String apiToken, String password) { this.login = login; @@ -134,7 +134,7 @@ public static GitHub connectAnonymously() { /*package*/ URL getApiURL(ApiVersion v, String tailApiUrl) throws IOException { if (oauthAccessToken != null) { // append the access token - tailApiUrl = tailApiUrl + "?access_token=" + oauthAccessToken; + tailApiUrl = tailApiUrl + (tailApiUrl.indexOf('?')>=0 ?'&':'?') + "access_token=" + oauthAccessToken; } return new URL(v.url+tailApiUrl); @@ -167,7 +167,6 @@ public static GitHub connectAnonymously() { private T _retrieve(String tailApiUrl, Class type, String method, boolean withAuth, ApiVersion v) throws IOException { while (true) {// loop while API rate limit is hit - HttpURLConnection uc = (HttpURLConnection) getApiURL(v,tailApiUrl).openConnection(); if (withAuth && this.oauthAccessToken == null) @@ -224,23 +223,16 @@ private T _retrieve(String tailApiUrl, Class type, String method, boolean public GHUser getMyself() throws IOException { requireCredential(); - GHUser u = null; - if (oauthAccessToken != null) { - - u = retrieve("/user/show", JsonUser.class).user; + GHUser u = retrieveWithAuth("/user/show", JsonUser.class).user; u.root = this; users.put(u.getLogin(), u); return u; - } - else { + } else { return getUser(login); } - - - } /** diff --git a/src/main/java/org/kohsuke/github/JsonUsers.java b/src/main/java/org/kohsuke/github/JsonUsers.java index 257f6001f0..60c123e0e3 100644 --- a/src/main/java/org/kohsuke/github/JsonUsers.java +++ b/src/main/java/org/kohsuke/github/JsonUsers.java @@ -34,8 +34,8 @@ class JsonUsers { public List users; - public Set toSet(GitHub root) throws IOException { - Set r = new HashSet(); + public GHPersonSet toSet(GitHub root) throws IOException { + GHPersonSet r = new GHPersonSet(); for (String u : users) r.add(root.getUser(u)); return r; diff --git a/src/main/java/org/kohsuke/github/Poster.java b/src/main/java/org/kohsuke/github/Poster.java index 29b024d5de..556044a986 100644 --- a/src/main/java/org/kohsuke/github/Poster.java +++ b/src/main/java/org/kohsuke/github/Poster.java @@ -94,7 +94,7 @@ public Poster with(String key, String value) { return _with(key, value); } - private Poster _with(String key, Object value) { + public Poster _with(String key, Object value) { if (value!=null) { args.add(new Entry(key,value)); } @@ -125,9 +125,13 @@ public T to(String tailApiUrl, Class type, String method) throws IOExcept uc.setRequestProperty("Content-type","application/x-www-form-urlencoded"); if (authenticate) { if (v==ApiVersion.V3) { - if (root.password==null) - throw new IllegalArgumentException("V3 API doesn't support API token"); - uc.setRequestProperty("Authorization", "Basic " + root.encodedAuthorization); + if (root.oauthAccessToken!=null) { + uc.setRequestProperty("Authorization", "token " + root.oauthAccessToken); + } else { + if (root.password==null) + throw new IllegalArgumentException("V3 API doesn't support API token"); + uc.setRequestProperty("Authorization", "Basic " + root.encodedAuthorization); + } } else { uc.setRequestProperty("Authorization", "Basic " + root.encodedAuthorization); } diff --git a/src/test/java/org/kohsuke/AppTest.java b/src/test/java/org/kohsuke/AppTest.java index 55d7d1c35e..7d8a1f8581 100644 --- a/src/test/java/org/kohsuke/AppTest.java +++ b/src/test/java/org/kohsuke/AppTest.java @@ -1,12 +1,11 @@ package org.kohsuke; import junit.framework.TestCase; +import org.kohsuke.github.GHHook; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHOrganization.Permission; -import org.kohsuke.github.GHPullRequest; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHTeam; -import org.kohsuke.github.GHUser; import org.kohsuke.github.GitHub; import java.io.IOException; @@ -38,6 +37,22 @@ public void testMembership() throws Exception { Set members = gitHub.getOrganization("jenkinsci").getRepository("violations-plugin").getCollaboratorNames(); System.out.println(members.contains("kohsuke")); } + + public void testMemberOrgs() throws Exception { + GitHub gitHub = GitHub.connect(); + Set o = gitHub.getUser("kohsuke").getOrganizations(); + System.out.println(o); + } + + public void tryHook() throws Exception { + GitHub gitHub = GitHub.connect(); + GHRepository r = gitHub.getMyself().getRepository("test2"); + GHHook hook = r.createWebHook(new URL("http://www.google.com/")); + System.out.println(hook); + + for (GHHook h : r.getHooks()) + h.delete(); + } public void testApp() throws IOException { GitHub gitHub = GitHub.connect();