Compare commits

..

42 Commits

Author SHA1 Message Date
Kohsuke Kawaguchi
c0dff74536 switched the Maven repository 2011-02-23 09:31:02 +09:00
Kohsuke Kawaguchi
7f82306908 [maven-release-plugin] prepare release github-api-1.5 2011-02-23 09:26:18 +09:00
Kohsuke Kawaguchi
7c599393bf added methods to parse timestamp 2011-02-23 06:58:14 +09:00
Kohsuke Kawaguchi
d1cd06aec4 incorporated the pagenation 2011-02-23 06:47:22 +09:00
Kohsuke Kawaguchi
13c9da9e91 missing the return statement 2010-12-16 15:31:29 -08:00
Kohsuke Kawaguchi
a827e51fa7 added the rename support. 2010-12-16 15:23:17 -08:00
Kohsuke Kawaguchi
637950c8be added a method to fork a repository into an organization. 2010-12-16 13:36:05 -08:00
Kohsuke Kawaguchi
fa40b625cc explicitly declare Wagon 2010-12-14 10:05:28 -08:00
Kohsuke Kawaguchi
15612e6bfd [maven-release-plugin] prepare for next development iteration 2010-12-14 09:54:23 -08:00
Kohsuke Kawaguchi
83a45f9adc [maven-release-plugin] prepare release github-api-1.4 2010-12-14 09:54:18 -08:00
Kohsuke Kawaguchi
d6b1aad91f added an operation to retrieve repositories from a team. 2010-12-12 21:21:31 -08:00
Kohsuke Kawaguchi
bbf045dbf1 bug fix 2010-12-12 21:17:08 -08:00
Kohsuke Kawaguchi
a282cd3f74 handle API rate limit 2010-12-12 10:30:24 -08:00
Kohsuke Kawaguchi
94c307238f unused method 2010-12-12 09:52:34 -08:00
Kohsuke Kawaguchi
ac1c003d54 simplified. 2010-12-12 09:52:00 -08:00
Kohsuke Kawaguchi
01257b4b86 simplified a bit 2010-12-12 09:50:14 -08:00
Kohsuke Kawaguchi
9484a5f2ac fixed a bug in createRepository 2010-12-12 09:33:09 -08:00
Kohsuke Kawaguchi
75144f1563 shouldn't be a part of test 2010-12-11 23:19:57 -08:00
Kohsuke Kawaguchi
f2271e8ee3 added an operation to create a team. 2010-12-11 22:35:03 -08:00
Kohsuke Kawaguchi
2e544ed8ed keeping up with GitHub API changes 2010-12-11 22:34:47 -08:00
Kohsuke Kawaguchi
0212dce1e8 support multiple values for the same key. 2010-12-11 22:26:04 -08:00
Kohsuke Kawaguchi
f96fda2d86 support deleting a repository from organization 2010-12-11 22:16:17 -08:00
Kohsuke Kawaguchi
e413912edd unified the handling of the password and API token. 2010-12-09 16:04:17 -08:00
Kohsuke Kawaguchi
c6558c6527 added a method to test credential. 2010-12-09 15:58:33 -08:00
Kohsuke Kawaguchi
a7c63e5d6c added another factory method 2010-12-09 15:31:51 -08:00
Kohsuke Kawaguchi
1f3b8866e7 added support for repository commit hook access 2010-12-09 10:00:54 -08:00
Kohsuke Kawaguchi
280b8d3b5a [maven-release-plugin] prepare for next development iteration 2010-11-23 21:52:30 -08:00
Kohsuke Kawaguchi
31028a31e6 [maven-release-plugin] prepare release github-api-1.3 2010-11-23 21:52:24 -08:00
Kohsuke Kawaguchi
efdabb819b shouldn't be public 2010-11-23 21:51:37 -08:00
Kohsuke Kawaguchi
e665365f11 introduced the common base type between GHOrganization and GHUser. 2010-11-23 21:51:14 -08:00
Kohsuke Kawaguchi
5159a21de1 doc improvement 2010-11-23 21:36:35 -08:00
Kohsuke Kawaguchi
ea97f8108f member addition and removal 2010-11-23 21:35:50 -08:00
Kohsuke Kawaguchi
9b1dd273fd added team support 2010-11-23 21:29:41 -08:00
Kohsuke Kawaguchi
7cd0d921cb adding support for creating repositories into an organization 2010-11-22 21:25:51 -08:00
Kohsuke Kawaguchi
959945a93d [maven-release-plugin] prepare for next development iteration 2010-04-19 09:38:25 -07:00
Kohsuke Kawaguchi
3080313aef [maven-release-plugin] prepare release github-api-1.2 2010-04-19 09:38:20 -07:00
Kohsuke Kawaguchi
0ddac59c9c added the fork support 2010-04-19 09:27:44 -07:00
Kohsuke Kawaguchi
131caf04e5 [maven-release-plugin] prepare for next development iteration 2010-04-18 22:24:12 -07:00
Kohsuke Kawaguchi
6c3884b94d [maven-release-plugin] prepare release github-api-1.1 2010-04-18 22:24:07 -07:00
Kohsuke Kawaguchi
540d473e42 bug fix 2010-04-18 22:21:30 -07:00
Kohsuke Kawaguchi
db717c86bb added following/follows support 2010-04-18 22:12:10 -07:00
Kohsuke Kawaguchi
4f9cace3fe [maven-release-plugin] prepare for next development iteration 2010-04-18 22:04:28 -07:00
17 changed files with 824 additions and 109 deletions

29
pom.xml
View File

@@ -3,16 +3,16 @@
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
<packaging>jar</packaging>
<version>1.0</version>
<version>1.5</version>
<name>GitHub API for Java</name>
<url>http://kohsuke.org/github-api/</url>
<description>GitHub API for Java</description>
<distributionManagement>
<repository>
<id>java.net-m2-repository</id>
<url>java-net:/maven2-repository/trunk/repository/</url>
</repository>
<distributionManagement>
<repository>
<id>maven.jenkins-ci.org</id>
<url>http://maven.jenkins-ci.org:8081/content/repositories/releases/</url>
</repository>
<site>
<id>kohsuke.org</id>
<url>scp://kohsuke.org/home/kohsuke/kohsuke.org/github-api/</url>
@@ -52,6 +52,11 @@
<artifactId>wagon-svn</artifactId>
<version>1.9</version>
</extension>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>1.0-beta-7</version>
</extension>
</extensions>
</build>
@@ -75,6 +80,18 @@
</licenses>
<dependencies>
<dependency>
<groupId>org.jvnet.hudson</groupId>
<artifactId>htmlunit</artifactId>
<version>2.6-hudson-2</version>
<exclusions>
<exclusion>
<!-- hides JDK DOM classes in Eclipse -->
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View File

@@ -0,0 +1,14 @@
package org.kohsuke.github;
/**
* @author Kohsuke Kawaguchi
*/
public class GHException extends RuntimeException {
public GHException(String message) {
super(message);
}
public GHException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,66 @@
package org.kohsuke.github;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
/**
* @author Kohsuke Kawaguchi
*/
public class GHOrganization extends GHPerson {
/**
* Creates a new repository.
*
* @return
* Newly created repository.
*/
public GHRepository createRepository(String name, String description, String homepage, String team, boolean isPublic) throws IOException {
// such API doesn't exist, so fall back to HTML scraping
WebClient wc = root.createWebClient();
HtmlPage pg = (HtmlPage)wc.getPage("https://github.com/organizations/"+login+"/repositories/new");
HtmlForm f = pg.getForms().get(1);
f.getInputByName("repository[name]").setValueAttribute(name);
f.getInputByName("repository[description]").setValueAttribute(description);
f.getInputByName("repository[homepage]").setValueAttribute(homepage);
f.getSelectByName("team_id").getOptionByText(team).setSelected(true);
f.submit(f.getButtonByCaption("Create Repository"));
return refreshRepository(name);
// GHRepository r = new Poster(root).withCredential()
// .with("name", name).with("description", description).with("homepage", homepage)
// .with("public", isPublic ? 1 : 0).to(root.getApiURL("/organizations/"+login+"/repos/create"), JsonRepository.class).repository;
// r.root = root;
// return r;
}
/**
* Teams by their names.
*/
public Map<String,GHTeam> getTeams() throws IOException {
return root.retrieveWithAuth("/organizations/"+login+"/teams",JsonTeams.class).toMap(this);
}
public enum Permission { ADMIN, PUSH, PULL }
/**
* Creates a new team and assigns the repositories.
*/
public GHTeam createTeam(String name, Permission p, Collection<GHRepository> repositories) throws IOException {
Poster post = new Poster(root).withCredential().with("team[name]", name).with("team[permission]", p.name().toLowerCase());
for (GHRepository r : repositories) {
post.with("team[repo_names][]",r.getOwnerName()+'/'+r.getName());
}
return post.to("/organizations/"+login+"/teams",JsonTeam.class).wrap(this);
}
public GHTeam createTeam(String name, Permission p, GHRepository... repositories) throws IOException {
return createTeam(name,p, Arrays.asList(repositories));
}
}

View File

@@ -0,0 +1,65 @@
package org.kohsuke.github;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
/**
* Common part of {@link GHUser} and {@link GHOrganization}.
*
* @author Kohsuke Kawaguchi
*/
public abstract class GHPerson {
/*package almost final*/ GitHub root;
protected String gravatar_id,login;
protected int public_gist_count,public_repo_count,following_count,id;
/**
* Repositories that this user owns.
*/
private transient Map<String,GHRepository> repositories;
/**
* Gets the repositories this user owns.
*/
public synchronized Map<String,GHRepository> getRepositories() throws IOException {
if (repositories==null) {
repositories = Collections.synchronizedMap(new TreeMap<String, GHRepository>());
for (int i=1; ; i++) {
Map<String, GHRepository> map = root.retrieve("/repos/show/" + login + "?page=" + i, JsonRepositories.class).wrap(root);
repositories.putAll(map);
if (map.isEmpty()) break;
}
}
return Collections.unmodifiableMap(repositories);
}
/**
* Fetches the repository of the given name from GitHub, and return it.
*/
protected GHRepository refreshRepository(String name) throws IOException {
if (repositories==null) getRepositories(); // fetch the base first
GHRepository r = root.retrieve("/repos/show/" + login + '/' + name, JsonRepository.class).wrap(root);
repositories.put(name,r);
return r;
}
public GHRepository getRepository(String name) throws IOException {
return getRepositories().get(name);
}
/**
* Gravatar ID of this user, like 0cb9832a01c22c083390f3c5dcb64105
*/
public String getGravatarId() {
return gravatar_id;
}
public String getLogin() {
return login;
}
}

View File

@@ -23,14 +23,28 @@
*/
package org.kohsuke.github;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlButton;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlInput;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import java.io.IOException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import static java.util.Arrays.asList;
import static java.util.Arrays.*;
/**
* A repository on GitHub.
@@ -43,6 +57,7 @@ public class GHRepository {
private String description, homepage, url, name, owner;
private boolean has_issues, has_wiki, fork, _private, has_downloads;
private int watchers,forks;
private String created_at, pushed_at;
public String getDescription() {
return description;
@@ -52,6 +67,9 @@ public class GHRepository {
return homepage;
}
/**
* URL of this repository, like 'http://github.com/kohsuke/hudson'
*/
public String getUrl() {
return url;
}
@@ -64,6 +82,10 @@ public class GHRepository {
return root.getUser(owner);
}
protected String getOwnerName() {
return owner;
}
public boolean hasIssues() {
return has_issues;
}
@@ -92,6 +114,23 @@ public class GHRepository {
return watchers;
}
public Date getPushedAt() {
return parseDate(pushed_at);
}
public Date getCreatedAt() {
return parseDate(created_at);
}
private Date parseDate(String timestamp) {
try {
return new SimpleDateFormat(TIME_FORMAT).parse(timestamp);
} catch (ParseException e) {
throw new IllegalStateException("Unable to parse the timestamp: "+pushed_at);
}
}
/**
* Gets the collaborators on this repository.
* This set always appear to include the owner.
@@ -120,10 +159,9 @@ public class GHRepository {
}
private void modifyCollaborators(Collection<GHUser> users, String op) throws IOException {
root.requireCredential();
verifyMine();
for (GHUser user : users) {
new Poster(root).withCredential().to(root.getApiURL("/repos/collaborators/"+name+ op +user.getLogin()));
new Poster(root).withCredential().to("/repos/collaborators/"+name+ op +user.getLogin());
}
}
@@ -131,22 +169,179 @@ public class GHRepository {
* Deletes this repository.
*/
public void delete() throws IOException {
root.requireCredential();
verifyMine();
Poster poster = new Poster(root).withCredential();
URL url = root.getApiURL("/repos/delete/" + name);
String url = "/repos/delete/" + owner +"/"+name;
DeleteToken token = poster.to(url, DeleteToken.class);
poster.with("delete_token",token.delete_token).to(url);
}
/**
* Forks this repository as your repository.
*
* @return
* Newly forked repository that belong to you.
*/
public GHRepository fork() throws IOException {
return new Poster(root).withCredential().to("/repos/fork/" + owner + "/" + name, JsonRepository.class).wrap(root);
}
/**
* Forks this repository into an organization.
*
* @return
* Newly forked repository that belong to you.
*/
public GHRepository forkTo(GHOrganization org) throws IOException {
WebClient wc = root.createWebClient();
HtmlPage pg = (HtmlPage)wc.getPage(getUrl());
for (HtmlForm f : pg.getForms()) {
if (!f.getActionAttribute().endsWith("/fork")) continue;
try {
if (org.getLogin().equals(f.getInputByName("organization").getValueAttribute())) {
// found it
f.submit((HtmlButton)f.getElementsByTagName("button").get(0));
return org.refreshRepository(name);
}
} catch (ElementNotFoundException e) {
// continue
}
}
throw new IllegalArgumentException("Either you don't have the privilege to fork into "+org.getLogin()+" or there's a bug in HTML scraping");
}
/**
* Rename this repository.
*/
public void renameTo(String newName) throws IOException {
WebClient wc = root.createWebClient();
HtmlPage pg = (HtmlPage)wc.getPage(getUrl()+"/admin");
for (HtmlForm f : pg.getForms()) {
if (!f.getActionAttribute().endsWith("/rename")) continue;
try {
f.getInputByName("name").setValueAttribute(newName);
f.submit((HtmlButton)f.getElementsByTagName("button").get(0));
name = newName;
return;
} catch (ElementNotFoundException e) {
// continue
}
}
throw new IllegalArgumentException("Either you don't have the privilege to rename "+owner+'/'+name+" or there's a bug in HTML scraping");
}
private void verifyMine() throws IOException {
if (!root.login.equals(owner))
throw new IOException("Operation not applicable to a repository owned by someone else: "+owner);
}
/**
* Returns a set that represents the post-commit hook URLs.
* The returned set is live, and changes made to them are reflected to GitHub.
*/
public Set<URL> getPostCommitHooks() {
return postCommitHooks;
}
/**
* Live set view of the post-commit hook.
*/
private final Set<URL> postCommitHooks = new AbstractSet<URL>() {
private List<URL> getPostCommitHooks() {
try {
verifyMine();
HtmlForm f = getForm();
List<URL> r = new ArrayList<URL>();
for (HtmlInput i : f.getInputsByName("urls[]")) {
String v = i.getValueAttribute();
if (v.length()==0) continue;
r.add(new URL(v));
}
return r;
} catch (IOException e) {
throw new GHException("Failed to retrieve post-commit hooks",e);
}
}
@Override
public Iterator<URL> iterator() {
return getPostCommitHooks().iterator();
}
@Override
public int size() {
return getPostCommitHooks().size();
}
@Override
public boolean add(URL url) {
try {
String u = url.toExternalForm();
verifyMine();
HtmlForm f = getForm();
List<HtmlInput> 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);
return true;
} catch (IOException e) {
throw new GHException("Failed to update post-commit hooks",e);
}
}
@Override
public boolean remove(Object o) {
try {
String u = ((URL)o).toExternalForm();
verifyMine();
HtmlForm f = getForm();
List<HtmlInput> 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);
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;
}
};
@Override
public String toString() {
return "Repository:"+owner+":"+name;
}
private static final String TIME_FORMAT = "yyyy/MM/dd HH:mm:ss ZZZZ";
}

View File

@@ -0,0 +1,66 @@
package org.kohsuke.github;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
/**
* A team in GitHub organization.
*
* @author Kohsuke Kawaguchi
*/
public class GHTeam {
private String name,permission;
private int id;
protected /*final*/ GHOrganization org;
public String getName() {
return name;
}
public String getPermission() {
return permission;
}
public int getId() {
return id;
}
/**
* Retrieves the current members.
*/
public Set<GHUser> getMembers() throws IOException {
return org.root.retrieveWithAuth(api("/members"),JsonUsersWithDetails.class).toSet(org.root);
}
public Map<String,GHRepository> getRepositories() throws IOException {
return org.root.retrieveWithAuth(api("/repositories"),JsonRepositories.class).wrap(org.root);
}
/**
* Adds a member to the team.
*/
public void add(GHUser u) throws IOException {
org.root.retrieveWithAuth(api("/members?name="+u.getLogin()),null, "POST");
}
/**
* Removes a member to the team.
*/
public void remove(GHUser u) throws IOException {
org.root.retrieveWithAuth(api("/members?name="+u.getLogin()),null, "DELETE");
}
public void add(GHRepository r) throws IOException {
org.root.retrieveWithAuth(api("/repositories?name="+r.getOwnerName()+'/'+r.getName()),null, "POST");
}
public void remove(GHRepository r) throws IOException {
org.root.retrieveWithAuth(api("/repositories?name="+r.getOwnerName()+'/'+r.getName()),null, "DELETE");
}
private String api(String tail) {
return "/teams/"+id+tail;
}
}

View File

@@ -24,35 +24,16 @@
package org.kohsuke.github;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import static org.kohsuke.github.GitHub.MAPPER;
import java.util.Set;
/**
* Represents an user of GitHub.
*
* @author Kohsuke Kawaguchi
*/
public class GHUser {
/*package almost final*/ GitHub root;
private String gravatar_id,name,company,location,created_at,blog,login,email;
private int public_gist_count,public_repo_count,following_count,id,followers_count;
/**
* Repositories that this user owns.
*/
private transient Map<String,GHRepository> repositories;
/**
* Gravatar ID of this user, like 0cb9832a01c22c083390f3c5dcb64105
*/
public String getGravatarId() {
return gravatar_id;
}
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"
@@ -124,23 +105,31 @@ public class GHUser {
}
/**
* Gets the repositories this user owns.
* Follow this user.
*/
public Map<String,GHRepository> getRepositories() throws IOException {
if (repositories==null) {
repositories = new TreeMap<String, GHRepository>();
URL url = new URL("http://github.com/api/v2/json/repos/show/" + login);
for (GHRepository r : MAPPER.readValue(url, JsonRepositories.class).repositories) {
r.root = root;
repositories.put(r.getName(),r);
}
}
return Collections.unmodifiableMap(repositories);
public void follow() throws IOException {
new Poster(root).withCredential().to("/user/follow/"+login);
}
public GHRepository getRepository(String name) throws IOException {
return getRepositories().get(name);
/**
* Unfollow this user.
*/
public void unfollow() throws IOException {
new Poster(root).withCredential().to("/user/unfollow/"+login);
}
/**
* Lists the users that this user is following
*/
public Set<GHUser> getFollows() throws IOException {
return root.retrieve("/user/show/"+login+"/following",JsonUsers.class).toSet(root);
}
/**
* Lists the users who are following this user.
*/
public Set<GHUser> getFollowers() throws IOException {
return root.retrieve("/user/show/"+login+"/followers",JsonUsers.class).toSet(root);
}
@Override

View File

@@ -23,22 +23,27 @@
*/
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 java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.ANY;
import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.NONE;
import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.*;
/**
* Root of the GitHub API.
@@ -47,13 +52,22 @@ import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.NONE;
*/
public class GitHub {
/*package*/ final String login;
/*package*/ final String token;
/*package*/ final String encodedAuthorization;
final String password;
private final Map<String,GHUser> users = new HashMap<String, GHUser>();
private final Map<String,GHOrganization> orgs = new HashMap<String, GHOrganization>();
private GitHub(String login, String apiToken) {
private GitHub(String login, String apiToken, String password) {
this.login = login;
this.token = apiToken;
this.password = password;
BASE64Encoder enc = new sun.misc.BASE64Encoder();
if (apiToken!=null || password!=null) {
String userpassword = apiToken!=null ? (login + "/token" + ":" + apiToken) : (login + ':'+password);
encodedAuthorization = enc.encode(userpassword.getBytes());
} else
encodedAuthorization = null;
}
/**
@@ -68,11 +82,15 @@ public class GitHub {
} finally {
IOUtils.closeQuietly(in);
}
return new GitHub(props.getProperty("login"),props.getProperty("token"));
return new GitHub(props.getProperty("login"),props.getProperty("token"),props.getProperty("password"));
}
public static GitHub connect(String login, String apiToken) throws IOException {
return new GitHub(login,apiToken);
return new GitHub(login,apiToken,null);
}
public static GitHub connect(String login, String apiToken, String password) throws IOException {
return new GitHub(login,apiToken,password);
}
/**
@@ -81,20 +99,67 @@ public class GitHub {
* All operations that requires authentication will fail.
*/
public static GitHub connectAnonymously() {
return new GitHub(null,null);
return new GitHub(null,null,null);
}
/*package*/ void requireCredential() {
if (login ==null || token ==null)
if (login==null || encodedAuthorization==null)
throw new IllegalStateException("This operation requires a credential but none is given to the GitHub constructor");
}
/*package*/ URL getApiURL(String tail) throws IOException {
return new URL("http://github.com/api/v2/json"+tail);
/*package*/ URL getApiURL(String tailApiUrl) throws IOException {
return new URL("http://github.com/api/v2/json"+tailApiUrl);
}
/*package*/ <T> T retrieve(String tail, Class<T> type) throws IOException {
return MAPPER.readValue(getApiURL(tail),type);
/*package*/ <T> T retrieve(String tailApiUrl, Class<T> type) throws IOException {
return _retrieve(tailApiUrl, type, "GET", false);
}
/*package*/ <T> T retrieveWithAuth(String tailApiUrl, Class<T> type) throws IOException {
return retrieveWithAuth(tailApiUrl,type,"GET");
}
/*package*/ <T> T retrieveWithAuth(String tailApiUrl, Class<T> type, String method) throws IOException {
return _retrieve(tailApiUrl, type, method, true);
}
private <T> T _retrieve(String tailApiUrl, Class<T> type, String method, boolean withAuth) throws IOException {
while (true) {// loop while API rate limit is hit
HttpURLConnection uc = (HttpURLConnection) getApiURL(tailApiUrl).openConnection();
if (withAuth)
uc.setRequestProperty("Authorization", "Basic " + encodedAuthorization);
uc.setRequestMethod(method);
try {
InputStreamReader r = new InputStreamReader(uc.getInputStream(), "UTF-8");
if (type==null) {
String data = IOUtils.toString(r);
return null;
}
return MAPPER.readValue(r,type);
} catch (IOException e) {
handleApiError(e,uc);
}
}
}
/**
* If the error is because of the API limit, wait 10 sec and return normally.
* Otherwise throw an exception reporting an error.
*/
/*package*/ void handleApiError(IOException e, HttpURLConnection uc) throws IOException {
if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) {
// API limit reached. wait 10 secs and return normally
try {
Thread.sleep(10000);
return;
} catch (InterruptedException _) {
throw (InterruptedIOException)new InterruptedIOException().initCause(e);
}
}
throw (IOException)new IOException(IOUtils.toString(uc.getErrorStream(),"UTF-8")).initCause(e);
}
/**
@@ -103,13 +168,36 @@ public class GitHub {
public GHUser getUser(String login) throws IOException {
GHUser u = users.get(login);
if (u==null) {
u = MAPPER.readValue(getApiURL("/user/show/"+login), JsonUser.class).user;
u = retrieve("/user/show/"+login,JsonUser.class).user;
u.root = this;
users.put(login,u);
}
return u;
}
/**
* Interns the given {@link GHUser}.
*/
protected GHUser getUser(GHUser orig) throws IOException {
GHUser u = users.get(orig.getLogin());
if (u==null) {
orig.root = this;
users.put(login,orig);
return orig;
}
return u;
}
public GHOrganization getOrganization(String name) throws IOException {
GHOrganization o = orgs.get(name);
if (o==null) {
o = retrieve("/organizations/"+name,JsonOrganization.class).organization;
o.root = this;
orgs.put(name,o);
}
return o;
}
/**
* Gets the {@link GHUser} that represents yourself.
*/
@@ -125,12 +213,33 @@ public class GitHub {
* Newly created repository.
*/
public GHRepository createRepository(String name, String description, String homepage, boolean isPublic) throws IOException {
requireCredential();
GHRepository r = new Poster(this).withCredential()
return new Poster(this).withCredential()
.with("name", name).with("description", description).with("homepage", homepage)
.with("public", isPublic ? 1 : 0).to(getApiURL("/repos/create"), JsonRepository.class).repository;
r.root = this;
return r;
.with("public", isPublic ? 1 : 0).to("/repos/create", JsonRepository.class).wrap(this);
}
/**
* Ensures that the credential is valid.
*/
public boolean isCredentialValid() throws IOException {
try {
retrieveWithAuth("/user/show",JsonUser.class);
return true;
} catch (IOException e) {
return false;
}
}
WebClient createWebClient() throws IOException {
WebClient wc = new WebClient();
wc.setJavaScriptEnabled(false);
wc.setCssEnabled(false);
HtmlPage pg = (HtmlPage)wc.getPage("https://github.com/login");
HtmlForm f = pg.getForms().get(0);
f.getInputByName("login").setValueAttribute(login);
f.getInputByName("password").setValueAttribute(password);
f.submit();
return wc;
}

View File

@@ -0,0 +1,8 @@
package org.kohsuke.github;
/**
* @author Kohsuke Kawaguchi
*/
class JsonOrganization {
public GHOrganization organization;
}

View File

@@ -24,10 +24,21 @@
package org.kohsuke.github;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* @author Kohsuke Kawaguchi
*/
class JsonRepositories {
public List<GHRepository> repositories;
public Map<String,GHRepository> wrap(GitHub root) {
Map<String,GHRepository> map = new TreeMap<String, GHRepository>();
for (GHRepository r : repositories) {
r.root = root;
map.put(r.getName(),r);
}
return map;
}
}

View File

@@ -28,4 +28,9 @@ package org.kohsuke.github;
*/
class JsonRepository {
public GHRepository repository;
public GHRepository wrap(GitHub root) {
repository.root = root;
return repository;
}
}

View File

@@ -0,0 +1,13 @@
package org.kohsuke.github;
/**
* @author Kohsuke Kawaguchi
*/
public class JsonTeam {
public GHTeam team;
GHTeam wrap(GHOrganization org) {
team.org = org;
return team;
}
}

View File

@@ -0,0 +1,21 @@
package org.kohsuke.github;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* @author Kohsuke Kawaguchi
*/
class JsonTeams {
public List<GHTeam> teams;
Map<String, GHTeam> toMap(GHOrganization org) {
Map<String, GHTeam> r = new TreeMap<String, GHTeam>();
for (GHTeam t : teams) {
t.org = org;
r.put(t.getName(),t);
}
return r;
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author Kohsuke Kawaguchi
*/
class JsonUsers {
public List<String> users;
public Set<GHUser> toSet(GitHub root) throws IOException {
Set<GHUser> r = new HashSet<GHUser>();
for (String u : users)
r.add(root.getUser(u));
return r;
}
}

View File

@@ -0,0 +1,20 @@
package org.kohsuke.github;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author Kohsuke Kawaguchi
*/
class JsonUsersWithDetails {
public List<GHUser> users;
public Set<GHUser> toSet(GitHub root) throws IOException {
Set<GHUser> r = new HashSet<GHUser>();
for (GHUser u : users)
r.add(root.getUser(u));
return r;
}
}

View File

@@ -29,14 +29,13 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ArrayList;
import java.util.List;
import static org.kohsuke.github.GitHub.MAPPER;
import static org.kohsuke.github.GitHub.*;
/**
* Handles HTTP POST.
@@ -44,14 +43,17 @@ import static org.kohsuke.github.GitHub.MAPPER;
*/
class Poster {
private final GitHub root;
private final Map<String,String> args = new HashMap<String, String>();
private final List<String> args = new ArrayList<String>();
private boolean authenticate;
Poster(GitHub root) {
this.root = root;
}
public Poster withCredential() {
return with("login",root.login).with("token",root.token);
root.requireCredential();
authenticate = true;
return this;
}
public Poster with(String key, int value) {
@@ -59,13 +61,18 @@ class Poster {
}
public Poster with(String key, String value) {
if (value!=null)
this.args.put(key,value);
if (value!=null) {
try {
args.add(URLEncoder.encode(key,"UTF-8")+'='+URLEncoder.encode(value,"UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new Error(e); // impossible
}
}
return this;
}
public void to(URL url) throws IOException {
to(url,null);
public void to(String tailApiUrl) throws IOException {
to(tailApiUrl,null);
}
/**
@@ -76,33 +83,41 @@ class Poster {
* @return
* {@link Reader} that reads the response.
*/
public <T> T to(URL url, Class<T> type) throws IOException {
HttpURLConnection uc = (HttpURLConnection) url.openConnection();
public <T> T to(String tailApiUrl, Class<T> type) throws IOException {
return to(tailApiUrl,type,"POST");
}
uc.setDoOutput(true);
uc.setRequestProperty("Content-type","application/x-www-form-urlencoded");
uc.setRequestMethod("POST");
public <T> T to(String tailApiUrl, Class<T> type, String method) throws IOException {
while (true) {// loop while API rate limit is hit
HttpURLConnection uc = (HttpURLConnection) root.getApiURL(tailApiUrl).openConnection();
uc.setDoOutput(true);
uc.setRequestProperty("Content-type","application/x-www-form-urlencoded");
if (authenticate)
uc.setRequestProperty("Authorization", "Basic " + root.encodedAuthorization);
uc.setRequestMethod(method);
StringBuilder body = new StringBuilder();
for (Entry<String, String> e : args.entrySet()) {
if (body.length()>0) body.append('&');
body.append(URLEncoder.encode(e.getKey(),"UTF-8"));
body.append('=');
body.append(URLEncoder.encode(e.getValue(),"UTF-8"));
}
StringBuilder body = new StringBuilder();
for (String e : args) {
if (body.length()>0) body.append('&');
body.append(e);
}
OutputStreamWriter o = new OutputStreamWriter(uc.getOutputStream(), "UTF-8");
o.write(body.toString());
o.close();
OutputStreamWriter o = new OutputStreamWriter(uc.getOutputStream(), "UTF-8");
o.write(body.toString());
o.close();
try {
InputStreamReader r = new InputStreamReader(uc.getInputStream(), "UTF-8");
if (type==null) return null;
return MAPPER.readValue(r,type);
} catch (IOException e) {
throw (IOException)new IOException(IOUtils.toString(uc.getErrorStream(),"UTF-8")).initCause(e);
try {
InputStreamReader r = new InputStreamReader(uc.getInputStream(), "UTF-8");
if (type==null) {
String data = IOUtils.toString(r);
return null;
}
return MAPPER.readValue(r,type);
} catch (IOException e) {
root.handleApiError(e,uc);
}
}
}
}

View File

@@ -1,21 +1,79 @@
package org.kohsuke;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHOrganization.Permission;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHTeam;
import org.kohsuke.github.GitHub;
import java.io.IOException;
import java.net.URL;
import java.util.Set;
/**
* Unit test for simple App.
*/
public class AppTest extends TestCase {
public void testApp() throws IOException {
GitHub hub = GitHub.connectAnonymously();
// hub.createRepository("test","test repository",null,true);
// hub.getUser("kohsuke").getRepository("test").delete();
public void testCredentialValid() throws IOException {
assertTrue(GitHub.connect().isCredentialValid());
assertFalse(GitHub.connect("totally","bogus").isCredentialValid());
}
System.out.println(hub.getUser("kohsuke").getRepository("hudson").getCollaborators());
public void testApp() throws IOException {
GitHub gitHub = GitHub.connect();
// tryRenaming(gitHub);
// tryOrgFork(gitHub);
// testOrganization(gitHub);
// testPostCommitHook(gitHub);
// tryTeamCreation(gitHub);
// t.add(gitHub.getMyself());
// System.out.println(t.getMembers());
// t.remove(gitHub.getMyself());
// System.out.println(t.getMembers());
// GHRepository r = GitHub.connect().getOrganization("HudsonLabs").createRepository("auto-test", "some description", "http://kohsuke.org/", "Plugin Developers", true);
// r.
// GitHub hub = GitHub.connectAnonymously();
//// hub.createRepository("test","test repository",null,true);
//// hub.getUser("kohsuke").getRepository("test").delete();
//
// System.out.println(hub.getUser("kohsuke").getRepository("hudson").getCollaborators());
}
private void tryRenaming(GitHub gitHub) throws IOException {
gitHub.getUser("kohsuke").getRepository("test").renameTo("test2");
}
private void tryOrgFork(GitHub gitHub) throws IOException {
GHOrganization o = gitHub.getOrganization("HudsonLabs");
System.out.println(gitHub.getUser("rtyler").getRepository("memcache-ada").forkTo(o).getUrl());
}
private void tryTeamCreation(GitHub gitHub) throws IOException {
GHOrganization o = gitHub.getOrganization("HudsonLabs");
GHTeam t = o.createTeam("auto team", Permission.PUSH);
t.add(o.getRepository("auto-test"));
}
private void testPostCommitHook(GitHub gitHub) throws IOException {
GHRepository r = gitHub.getMyself().getRepository("foo");
Set<URL> hooks = r.getPostCommitHooks();
hooks.add(new URL("http://kohsuke.org/test"));
System.out.println(hooks);
hooks.remove(new URL("http://kohsuke.org/test"));
System.out.println(hooks);
}
private void testOrganization(GitHub gitHub) throws IOException {
GHOrganization labs = gitHub.getOrganization("HudsonLabs");
GHTeam t = labs.getTeams().get("Core Developers");
t.add(labs.getRepository("xyz"));
}
}