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 extends T> 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();