Compare commits

...

35 Commits

Author SHA1 Message Date
Kohsuke Kawaguchi
8b428f2c93 whitespace only changes for consistent indentation 2015-03-22 11:14:19 -07:00
Kohsuke Kawaguchi
10238dbcd3 Explaining why this code is the way it is. 2015-03-22 11:13:40 -07:00
Kohsuke Kawaguchi
6229e0928d Revert "Set credentials file according to documentation"
This reverts commit 0bf81f4fb9.

The point of this is to allow me to use a separate account to avoid
corrupting my event stream. GitHub.connect() does the standard handling
for those who are not me.
2015-03-22 11:11:03 -07:00
Kohsuke Kawaguchi
5c7b259fe9 Using the latest 2015-03-22 11:02:17 -07:00
Kohsuke Kawaguchi
cc84c867c0 I think our coverage is pretty good now 2015-03-22 11:01:02 -07:00
Kohsuke Kawaguchi
5bf252e12d Added markdown support
Fixes issue #165
2015-03-22 11:00:57 -07:00
Kohsuke Kawaguchi
75512ff66a Turns out the interning of GHUser wasn't working at all!
Fixes issue #166.
2015-03-22 10:38:57 -07:00
Kohsuke Kawaguchi
6f4832476a test case for #162 2015-03-22 10:21:16 -07:00
Kohsuke Kawaguchi
86b0d27299 added a method to read content.
This also fixes #162.
2015-03-22 10:21:09 -07:00
Kohsuke Kawaguchi
5a8845f7f6 added a method to return the raw unprocessed body 2015-03-22 09:17:49 -07:00
Kohsuke Kawaguchi
709e47f32f Added getDownloadUrl() method 2015-03-22 09:10:34 -07:00
Kohsuke Kawaguchi
77590b4eb3 eliminate the need for path manipulation and consolidate them to 'with' 2015-03-21 16:52:49 -07:00
Kohsuke Kawaguchi
72fc313135 improved error handling 2015-03-21 16:43:11 -07:00
Kohsuke Kawaguchi
39b32cee2e Implemented /repositories
Fixed issue #157
2015-03-21 16:35:34 -07:00
Kohsuke Kawaguchi
bdcee7c052 Merge pull request #160 with some modifications
Strategy pattern is better for API rate limit handling as the current
behavior is quite valid for batch applications.

I'm not taking OkHttp version change so as not to introduce any unneeded
version requirement.
2015-03-17 07:44:36 -07:00
Kohsuke Kawaguchi
4093e53b5b Implemented a strategy pattern to let the client determine API rate limit behavior.
The default is set to the backward compatible behaviour.
2015-03-17 07:43:51 -07:00
Kanstantsin Shautsou
a4c1c8de24 Provide reset date info for rate limit 2015-03-17 07:31:05 -07:00
Kohsuke Kawaguchi
7ed234c875 Standardize environment variable names
... that are more like typical environment variables. Reasonably unique and upper case.

Deprecate other methods. The point of a connector method is to make sure all clients of the same library uses the same environments, thereby eliminating the pain of setting credentials per app.

Allowing the app to specify the environment variable names defeat this purpose.
2015-03-15 13:17:57 -07:00
Kohsuke Kawaguchi
0359160ac6 Avoided using JDK6 method 2015-03-15 13:07:21 -07:00
Kohsuke Kawaguchi
2478dad9b5 Simplification 2015-03-15 13:04:50 -07:00
Kohsuke Kawaguchi
690292352b Simplification
But this method is insane!
2015-03-15 13:02:28 -07:00
Kohsuke Kawaguchi
271d18cddc simplification 2015-03-15 13:01:35 -07:00
Kohsuke Kawaguchi
e1465639e7 Merge pull request #156 from ashwanthkumar/endpoint-from-properties
Picking endpoint from the properties file and environment variables
2015-03-15 19:55:32 +00:00
Kohsuke Kawaguchi
ce7ca59339 Merge pull request #155 2015-03-15 12:49:30 -07:00
Kohsuke Kawaguchi
76610b25d7 Promoted GHTreeEntry to the top-level 2015-03-15 12:49:14 -07:00
Kohsuke Kawaguchi
dfce0bda7c prefer list over raw array 2015-03-15 12:48:27 -07:00
Ashwanth Kumar
41c0dd9727 Fixing the indentation for enpointVariableName 2015-03-13 21:26:49 +05:30
Ashwanth Kumar
232c0389d3 Adding fromEnvironment to maintain backword compatibility 2015-03-13 21:26:19 +05:30
Oleg Nenashev
d95c8a4ab0 Merge pull request #161 from khoa-nd/master
Add method to get the list of languages using in repository
2015-03-13 17:58:01 +03:00
khoa-nd
374fdb37e1 Change type of language bytes from Integer to Long 2015-03-06 09:11:03 +07:00
khoa-nd
f78530636e Add method to get the list of languages using in repository 2015-03-05 15:53:02 +07:00
Kanstantsin Shautsou
0bf81f4fb9 Set credentials file according to documentation 2015-03-03 19:23:06 +03:00
Kohsuke Kawaguchi
dcc3b7f36b [maven-release-plugin] prepare for next development iteration 2015-03-02 09:10:52 -08:00
Ashwanth Kumar
a716a59489 Picking endpoint from the properties file and environment variables
Helps seemless switching between public github and enterprise without any code changes
2015-02-22 09:41:58 +05:30
Daniel
bad0d1bbcf implementing github trees as described https://developer.github.com/v3/git/trees/#get-a-tree-recursively 2015-02-18 14:58:27 +01:00
16 changed files with 627 additions and 162 deletions

View File

@@ -3,11 +3,11 @@
<parent>
<groupId>org.kohsuke</groupId>
<artifactId>pom</artifactId>
<version>12</version>
<version>14</version>
</parent>
<artifactId>github-api</artifactId>
<version>1.63</version>
<version>1.64-SNAPSHOT</version>
<name>GitHub API for Java</name>
<url>http://github-api.kohsuke.org/</url>
<description>GitHub API for Java</description>
@@ -16,7 +16,7 @@
<connection>scm:git:git@github.com/kohsuke/${project.artifactId}.git</connection>
<developerConnection>scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git</developerConnection>
<url>http://${project.artifactId}.kohsuke.org/</url>
<tag>github-api-1.63</tag>
<tag>HEAD</tag>
</scm>
<distributionManagement>

View File

@@ -1,8 +1,10 @@
package org.kohsuke.github;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.io.InputStream;
import javax.xml.bind.DatatypeConverter;
@@ -26,6 +28,7 @@ public class GHContent {
private String url; // this is the API url
private String git_url; // this is the Blob url
private String html_url; // this is the UI
private String download_url;
public GHRepository getOwner() {
return owner;
@@ -58,35 +61,34 @@ public class GHContent {
/**
* Retrieve the decoded content that is stored at this location.
*
* <p>
* Due to the nature of GitHub's API, you're not guaranteed that
* the content will already be populated, so this may trigger
* network activity, and can throw an IOException.
**/
*
* @deprecated
* Use {@link #read()}
*/
public String getContent() throws IOException {
return new String(DatatypeConverter.parseBase64Binary(getEncodedContent()));
}
/**
* Retrieve the raw content that is stored at this location.
* Retrieve the base64-encoded content that is stored at this location.
*
* <p>
* Due to the nature of GitHub's API, you're not guaranteed that
* the content will already be populated, so this may trigger
* network activity, and can throw an IOException.
**/
*
* @deprecated
* Use {@link #read()}
*/
public String getEncodedContent() throws IOException {
if (content != null)
if (content!=null)
return content;
GHContent retrievedContent = owner.getFileContent(path);
this.size = retrievedContent.size;
this.sha = retrievedContent.sha;
this.content = retrievedContent.content;
this.url = retrievedContent.url;
this.git_url = retrievedContent.git_url;
this.html_url = retrievedContent.html_url;
return content;
else
return Base64.encodeBase64String(IOUtils.toByteArray(read()));
}
public String getUrl() {
@@ -101,6 +103,18 @@ public class GHContent {
return html_url;
}
/**
* Retrieves the actual content stored here.
*/
public InputStream read() throws IOException {
return new Requester(owner.root).read(getDownloadUrl());
}
/**
* URL to retrieve the raw content of the file. Null if this is a directory.
*/
public String getDownloadUrl() { return download_url; }
public boolean isFile() {
return "file".equals(type);
}

View File

@@ -1,5 +1,7 @@
package org.kohsuke.github;
import java.util.Date;
/**
* Rate limit.
* @author Kohsuke Kawaguchi
@@ -10,12 +12,28 @@ public class GHRateLimit {
*/
public int remaining;
/**
* Alotted API call per hour.
* Allotted API call per hour.
*/
public int limit;
/**
* The time at which the current rate limit window resets in UTC epoch seconds.
*/
public Date reset;
/**
* Non-epoch date
*/
public Date getResetDate() {
return new Date(reset.getTime() * 1000);
}
@Override
public String toString() {
return remaining+"/"+limit;
return "GHRateLimit{" +
"remaining=" + remaining +
", limit=" + limit +
", resetDate=" + getResetDate() +
'}';
}
}

View File

@@ -30,7 +30,9 @@ import org.apache.commons.lang.StringUtils;
import javax.xml.bind.DatatypeConverter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.Reader;
import java.net.URL;
import java.util.*;
@@ -285,6 +287,18 @@ public class GHRepository extends GHObject {
};
}
/**
* List languages for the specified repository.
* The value on the right of a language is the number of bytes of code written in that language.
* {
"C": 78769,
"Python": 7769
}
*/
public Map<String,Long> listLanguages() throws IOException {
return root.retrieve().to(getApiTailUrl("languages"), HashMap.class);
}
public String getOwnerName() {
return owner.login;
}
@@ -641,6 +655,35 @@ public class GHRepository extends GHObject {
return root.retrieve().to(String.format("/repos/%s/%s/git/refs/%s", owner.login, name, refName), GHRef.class).wrap(root);
}
/**
* Retrive a tree of the given type for the current GitHub repository.
*
* @param sha - sha number or branch name ex: "master"
* @return refs matching the request type
* @throws IOException
* on failure communicating with GitHub, potentially due to an
* invalid tree type being requested
*/
public GHTree getTree(String sha) throws IOException {
String url = String.format("/repos/%s/%s/git/trees/%s", owner.login, name, sha);
return root.retrieve().to(url, GHTree.class).wrap(root);
}
/**
* Retrieves the tree for the current GitHub repository, recursively as described in here:
* https://developer.github.com/v3/git/trees/#get-a-tree-recursively
*
* @param sha - sha number or branch name ex: "master"
* @param recursive use 1
* @throws IOException
* on failure communicating with GitHub, potentially due to an
* invalid tree type being requested
*/
public GHTree getTreeRecursive(String sha, int recursive) throws IOException {
String url = String.format("/repos/%s/%s/git/trees/%s?recursive=%d", owner.login, name, sha, recursive);
return root.retrieve().to(url, GHTree.class).wrap(root);
}
/**
* Gets a commit object in this repository.
*/
public GHCommit getCommit(String sha1) throws IOException {
@@ -988,10 +1031,7 @@ public class GHRepository extends GHObject {
Requester requester = root.retrieve();
String target = getApiTailUrl("contents/" + path);
if (ref != null)
target = target + "?ref=" + ref;
return requester.to(target, GHContent.class).wrap(this);
return requester.with("ref",ref).to(target, GHContent.class).wrap(this);
}
public List<GHContent> getDirectoryContent(String path) throws IOException {
@@ -1002,10 +1042,7 @@ public class GHRepository extends GHObject {
Requester requester = root.retrieve();
String target = getApiTailUrl("contents/" + path);
if (ref != null)
target = target + "?ref=" + ref;
GHContent[] files = requester.to(target, GHContent[].class);
GHContent[] files = requester.with("ref",ref).to(target, GHContent[].class);
GHContent.wrap(files, this);
@@ -1114,7 +1151,25 @@ public class GHRepository extends GHObject {
return contributions;
}
}
/**
* Render a Markdown document.
*
* In {@linkplain MarkdownMode#GFM GFM mode}, issue numbers and user mentions
* are linked accordingly.
*
* @see GitHub#renderMarkdown(String)
*/
public Reader renderMarkdown(String text, MarkdownMode mode) throws IOException {
return new InputStreamReader(
new Requester(root)
.with("text", text)
.with("mode",mode==null?null:mode.toString())
.with("context", getFullName())
.read("/markdown"),
"UTF-8");
}
@Override

View File

@@ -0,0 +1,58 @@
package org.kohsuke.github;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Provides information for Git Trees
* https://developer.github.com/v3/git/trees/
*
* @author Daniel Teixeira - https://github.com/ddtxra
* @see GHRepository#getTree(String)
*/
public class GHTree {
/* package almost final */GitHub root;
private boolean truncated;
private String sha, url;
private GHTreeEntry[] tree;
/**
* The SHA for this trees
*/
public String getSha() {
return sha;
}
/**
* Return an array of entries of the trees
* @return
*/
public List<GHTreeEntry> getTree() {
return Collections.unmodifiableList(Arrays.asList(tree));
}
/**
* Returns true if the number of items in the tree array exceeded the GitHub maximum limit.
* @return true true if the number of items in the tree array exceeded the GitHub maximum limit otherwise false.
*/
public boolean isTruncated() {
return truncated;
}
/**
* The API URL of this tag, such as
* "url": "https://api.github.com/repos/octocat/Hello-World/trees/fc6274d15fa3ae2ab983129fb037999f264ba9a7",
*/
public URL getUrl() {
return GitHub.parseURL(url);
}
/* package */GHTree wrap(GitHub root) {
this.root = root;
return this;
}
}

View File

@@ -0,0 +1,71 @@
package org.kohsuke.github;
import java.net.URL;
/**
* Provides information for Git Trees
* https://developer.github.com/v3/git/trees/
*
* @author Daniel Teixeira - https://github.com/ddtxra
* @see GHTree
*/
public class GHTreeEntry {
private String path, mode, type, sha, url;
private long size;
/**
* Get the path such as
* "subdir/file.txt"
*
* @return the path
*/
public String getPath() {
return path;
}
/**
* Get mode such as
* 100644
*
* @return the mode
*/
public String getMode() {
return mode;
}
/**
* Gets the size of the file, such as
* 132
* @return The size of the path or 0 if it is a directory
*/
public long getSize() {
return size;
}
/**
* Gets the type such as:
* "blob"
*
* @return The type
*/
public String getType() {
return type;
}
/**
* SHA1 of this object.
*/
public String getSha() {
return sha;
}
/**
* API URL to this Git data, such as
* https://api.github.com/repos/jenkinsci
* /jenkins/git/commits/b72322675eb0114363a9a86e9ad5a170d1d07ac0
*/
public URL getUrl() {
return GitHub.parseURL(url);
}
}

View File

@@ -26,8 +26,10 @@ package org.kohsuke.github;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
@@ -75,6 +77,8 @@ public class GitHub {
private final String apiUrl;
/*package*/ final RateLimitHandler rateLimitHandler;
private HttpConnector connector = HttpConnector.DEFAULT;
/**
@@ -113,7 +117,7 @@ public class GitHub {
* @param connector
* HttpConnector to use. Pass null to use default connector.
*/
/* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector) throws IOException {
/* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector, RateLimitHandler rateLimitHandler) throws IOException {
if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize
this.apiUrl = apiUrl;
if (null != connector) this.connector = connector;
@@ -132,6 +136,7 @@ public class GitHub {
if (login==null && encodedAuthorization!=null)
login = getMyself().getLogin();
this.login = login;
this.rateLimitHandler = rateLimitHandler;
}
/**
@@ -290,7 +295,7 @@ public class GitHub {
GHUser u = users.get(orig.getLogin());
if (u==null) {
orig.root = this;
users.put(login,orig);
users.put(orig.getLogin(),orig);
return orig;
}
return u;
@@ -331,27 +336,27 @@ public class GitHub {
return r;
}
/**
* Gets complete map of organizations/teams that current user belongs to.
*
* Leverages the new GitHub API /user/teams made available recently to
* get in a single call the complete set of organizations, teams and permissions
* in a single call.
*/
public Map<String, Set<GHTeam>> getMyTeams() throws IOException {
Map<String, Set<GHTeam>> allMyTeams = new HashMap<String, Set<GHTeam>>();
for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) {
team.wrapUp(this);
String orgLogin = team.getOrganization().getLogin();
Set<GHTeam> teamsPerOrg = allMyTeams.get(orgLogin);
if (teamsPerOrg == null) {
teamsPerOrg = new HashSet<GHTeam>();
}
teamsPerOrg.add(team);
allMyTeams.put(orgLogin, teamsPerOrg);
/**
* Gets complete map of organizations/teams that current user belongs to.
*
* Leverages the new GitHub API /user/teams made available recently to
* get in a single call the complete set of organizations, teams and permissions
* in a single call.
*/
public Map<String, Set<GHTeam>> getMyTeams() throws IOException {
Map<String, Set<GHTeam>> allMyTeams = new HashMap<String, Set<GHTeam>>();
for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) {
team.wrapUp(this);
String orgLogin = team.getOrganization().getLogin();
Set<GHTeam> teamsPerOrg = allMyTeams.get(orgLogin);
if (teamsPerOrg == null) {
teamsPerOrg = new HashSet<GHTeam>();
}
teamsPerOrg.add(team);
allMyTeams.put(orgLogin, teamsPerOrg);
}
return allMyTeams;
}
return allMyTeams;
}
/**
* Public events visible to you. Equivalent of what's displayed on https://github.com/
@@ -438,6 +443,53 @@ public class GitHub {
return new GHIssueSearchBuilder(this);
}
/**
* This provides a dump of every public repository, in the order that they were created.
* @see <a href="https://developer.github.com/v3/repos/#list-all-public-repositories">documentation</a>
*/
public PagedIterable<GHRepository> listAllPublicRepositories() {
return listAllPublicRepositories(null);
}
/**
* This provides a dump of every public repository, in the order that they were created.
*
* @param since
* The integer ID of the last Repository that youve seen. See {@link GHRepository#getId()}
* @see <a href="https://developer.github.com/v3/repos/#list-all-public-repositories">documentation</a>
*/
public PagedIterable<GHRepository> listAllPublicRepositories(final String since) {
return new PagedIterable<GHRepository>() {
public PagedIterator<GHRepository> iterator() {
return new PagedIterator<GHRepository>(retrieve().with("since",since).asIterator("/repositories", GHRepository[].class)) {
@Override
protected void wrapUp(GHRepository[] page) {
for (GHRepository c : page)
c.wrap(GitHub.this);
}
};
}
};
}
/**
* Render a Markdown document in raw mode.
*
* <p>
* It takes a Markdown document as plaintext and renders it as plain Markdown
* without a repository context (just like a README.md file is rendered this
* is the simplest way to preview a readme online).
*
* @see GHRepository#renderMarkdown(String, MarkdownMode)
*/
public Reader renderMarkdown(String text) throws IOException {
return new InputStreamReader(
new Requester(this)
.with(new ByteArrayInputStream(text.getBytes("UTF-8")))
.contentType("text/plain;charset=UTF-8")
.read("/markdown/raw"),
"UTF-8");
}
/*package*/ static URL parseURL(String s) {
try {

View File

@@ -9,102 +9,119 @@ import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.util.Map;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Properties;
/**
*
*
* @since 1.59
*/
public class GitHubBuilder {
private String endpoint = GitHub.GITHUB_URL;
// default scoped so unit tests can read them.
/* private */ String endpoint = GitHub.GITHUB_URL;
/* private */ String user;
/* private */ String password;
/* private */ String oauthToken;
private HttpConnector connector;
private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
public GitHubBuilder() {
}
/**
* First check if the credentials are configured using the ~/.github properties file.
*
*
* If no user is specified it means there is no configuration present so check the environment instead.
*
*
* If there is still no user it means there are no credentials defined and throw an IOException.
*
*
* @return the configured Builder from credentials defined on the system or in the environment.
*
*
* @throws IOException If there are no credentials defined in the ~/.github properties file or the process environment.
*/
public static GitHubBuilder fromCredentials() throws IOException {
Exception cause = null;
GitHubBuilder builder;
try {
builder = fromPropertyFile();
if (builder.user != null)
return builder;
else {
// this is the case where the ~/.github file exists but has no content.
builder = fromEnvironment();
if (builder.user != null)
return builder;
else
throw new IOException("Failed to resolve credentials from ~/.github or the environment.");
}
} catch (FileNotFoundException e) {
builder = fromEnvironment();
if (builder.user != null)
return builder;
else
throw new IOException("Failed to resolve credentials from ~/.github or the environment.", e);
// fall through
cause = e;
}
builder = fromEnvironment();
if (builder.user != null)
return builder;
else
throw (IOException)new IOException("Failed to resolve credentials from ~/.github or the environment.").initCause(cause);
}
/**
* @deprecated
* Use {@link #fromEnvironment()} to pick up standard set of environment variables, so that
* different clients of this library will all recognize one consistent set of coordinates.
*/
public static GitHubBuilder fromEnvironment(String loginVariableName, String passwordVariableName, String oauthVariableName) throws IOException {
Properties env = new Properties();
Object loginValue = System.getenv(loginVariableName);
if (loginValue != null)
env.put("login", loginValue);
Object passwordValue = System.getenv(passwordVariableName);
if (passwordValue != null)
env.put("password", passwordValue);
Object oauthValue = System.getenv(oauthVariableName);
if (oauthValue != null)
env.put("oauth", oauthValue);
return fromProperties(env);
return fromEnvironment(loginVariableName, passwordVariableName, oauthVariableName, "");
}
private static void loadIfSet(String envName, Properties p, String propName) {
String v = System.getenv(envName);
if (v != null)
p.put(propName, v);
}
/**
* @deprecated
* Use {@link #fromEnvironment()} to pick up standard set of environment variables, so that
* different clients of this library will all recognize one consistent set of coordinates.
*/
public static GitHubBuilder fromEnvironment(String loginVariableName, String passwordVariableName, String oauthVariableName, String endpointVariableName) throws IOException {
Properties env = new Properties();
loadIfSet(loginVariableName,env,"login");
loadIfSet(passwordVariableName,env,"password");
loadIfSet(oauthVariableName,env,"oauth");
loadIfSet(endpointVariableName,env,"endpoint");
return fromProperties(env);
}
/**
* Creates {@link GitHubBuilder} by picking up coordinates from environment variables.
*
* <p>
* The following environment variables are recognized:
*
* <ul>
* <li>GITHUB_LOGIN: username like 'kohsuke'
* <li>GITHUB_PASSWORD: raw password
* <li>GITHUB_OAUTH: OAuth token to login
* <li>GITHUB_ENDPOINT: URL of the API endpoint
* </ul>
*
* <p>
* See class javadoc for the relationship between these coordinates.
*
* <p>
* For backward compatibility, the following environment variables are recognized but discouraged:
* login, password, oauth
*/
public static GitHubBuilder fromEnvironment() throws IOException {
Properties props = new Properties();
Map<String, String> env = System.getenv();
for (Map.Entry<String, String> element : env.entrySet()) {
props.put(element.getKey(), element.getValue());
}
for (Entry<String, String> e : System.getenv().entrySet()) {
String name = e.getKey().toLowerCase(Locale.ENGLISH);
if (name.startsWith("github_")) name=name.substring(7);
props.put(name,e.getValue());
}
return fromProperties(props);
}
@@ -113,7 +130,7 @@ public class GitHubBuilder {
File propertyFile = new File(homeDir, ".github");
return fromPropertyFile(propertyFile.getPath());
}
public static GitHubBuilder fromPropertyFile(String propertyFileName) throws IOException {
Properties props = new Properties();
FileInputStream in = null;
@@ -131,6 +148,7 @@ public class GitHubBuilder {
GitHubBuilder self = new GitHubBuilder();
self.withOAuthToken(props.getProperty("oauth"), props.getProperty("login"));
self.withPassword(props.getProperty("login"), props.getProperty("password"));
self.withEndpoint(props.getProperty("endpoint", GitHub.GITHUB_URL));
return self;
}
@@ -155,6 +173,10 @@ public class GitHubBuilder {
this.connector = connector;
return this;
}
public GitHubBuilder withRateLimitHandler(RateLimitHandler handler) {
this.rateLimitHandler = handler;
return this;
}
/**
* Configures {@linkplain #withConnector(HttpConnector) connector}
@@ -170,6 +192,6 @@ public class GitHubBuilder {
}
public GitHub build() throws IOException {
return new GitHub(endpoint, user, oauthToken, password, connector);
return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler);
}
}

View File

@@ -0,0 +1,29 @@
package org.kohsuke.github;
import java.util.Locale;
/**
* Rendering mode of markdown.
*
* @author Kohsuke Kawaguchi
* @see GitHub#renderMarkdown(String)
* @see GHRepository#renderMarkdown(String, MarkdownMode)
*/
public enum MarkdownMode {
/**
* Render a document as plain Markdown, just like README files are rendered.
*/
MARKDOWN,
/**
* Render a document as user-content, e.g. like user comments or issues are rendered.
* In GFM mode, hard line breaks are always taken into account, and issue and user
* mentions are linked accordingly.
*
* @see GHRepository#renderMarkdown(String, MarkdownMode)
*/
GFM;
public String toString() {
return name().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,61 @@
package org.kohsuke.github;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
/**
* Pluggable strategy to determine what to do when the API rate limit is reached.
*
* @author Kohsuke Kawaguchi
* @see GitHubBuilder#withRateLimitHandler(RateLimitHandler)
*/
public abstract class RateLimitHandler {
/**
* Called when the library encounters HTTP error indicating that the API rate limit is reached.
*
* <p>
* Any exception thrown from this method will cause the request to fail, and the caller of github-api
* will receive an exception. If this method returns normally, another request will be attempted.
* For that to make sense, the implementation needs to wait for some time.
*
* @see <a href="https://developer.github.com/v3/#rate-limiting">API documentation from GitHub</a>
* @param e
* Exception from Java I/O layer. If you decide to fail the processing, you can throw
* this exception (or wrap this exception into another exception and throw it.)
* @param uc
* Connection that resulted in an error. Useful for accessing other response headers.
*/
public abstract void onError(IOException e, HttpURLConnection uc) throws IOException;
/**
* Block until the API rate limit is reset. Useful for long-running batch processing.
*/
public static final RateLimitHandler WAIT = new RateLimitHandler() {
@Override
public void onError(IOException e, HttpURLConnection uc) throws IOException {
try {
Thread.sleep(parseWaitTime(uc));
} catch (InterruptedException _) {
throw (InterruptedIOException)new InterruptedIOException().initCause(e);
}
}
private long parseWaitTime(HttpURLConnection uc) {
String v = uc.getHeaderField("X-RateLimit-Reset");
if (v==null) return 10000; // can't tell
return Math.max(10000, Long.parseLong(v)*1000 - System.currentTimeMillis());
}
};
/**
* Fail immediately.
*/
public static final RateLimitHandler FAIL = new RateLimitHandler() {
@Override
public void onError(IOException e, HttpURLConnection uc) throws IOException {
throw (IOException)new IOException("API rate limit reached").initCause(e);
}
};
}

View File

@@ -29,7 +29,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
@@ -52,10 +51,9 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.apache.commons.io.IOUtils;
import javax.net.ssl.HttpsURLConnection;
/**
* A builder pattern for making HTTP call and parsing its output.
*
@@ -190,6 +188,14 @@ class Requester {
private <T> T _to(String tailApiUrl, Class<T> type, T instance) throws IOException {
while (true) {// loop while API rate limit is hit
if (method.equals("GET") && !args.isEmpty()) {
StringBuilder qs=new StringBuilder();
for (Entry arg : args) {
qs.append(qs.length()==0 ? '?' : '&');
qs.append(arg.key).append('=').append(URLEncoder.encode(arg.value.toString(),"UTF-8"));
}
tailApiUrl += qs.toString();
}
HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl));
buildRequest(uc);
@@ -238,6 +244,20 @@ class Requester {
}
}
public InputStream read(String tailApiUrl) throws IOException {
while (true) {// loop while API rate limit is hit
HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl));
buildRequest(uc);
try {
return wrapStream(uc,uc.getInputStream());
} catch (IOException e) {
handleApiError(e,uc);
}
}
}
private void buildRequest(HttpURLConnection uc) throws IOException {
if (!method.equals("GET")) {
uc.setDoOutput(true);
@@ -396,7 +416,11 @@ class Requester {
r = new InputStreamReader(wrapStream(uc, uc.getInputStream()), "UTF-8");
String data = IOUtils.toString(r);
if (type!=null)
return MAPPER.readValue(data,type);
try {
return MAPPER.readValue(data,type);
} catch (JsonMappingException e) {
throw (IOException)new IOException("Failed to deserialize "+data).initCause(e);
}
if (instance!=null)
return MAPPER.readerForUpdating(instance).<T>readValue(data);
return null;
@@ -417,18 +441,11 @@ class Requester {
}
/**
* If the error is because of the API limit, wait 10 sec and return normally.
* Otherwise throw an exception reporting an error.
* Handle API error by either throwing it or by returning normally to retry.
*/
/*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);
}
root.rateLimitHandler.onError(e,uc);
}
if (e instanceof FileNotFoundException)

View File

@@ -9,8 +9,7 @@ are used in favor of using string handle (such as `GHUser.isMemberOf(GHOrganizat
The library supports both github.com and GitHub Enterprise.
There are some corners of the GitHub API that's not yet implemented, but
the library is implemented with the right abstractions and libraries to make it very easy to improve the coverage.
Most of the GitHub APIs are covered, although there are some corners that are still not yet implemented.
Sample Usage
-----

View File

@@ -1,12 +1,10 @@
package org.kohsuke.github;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Before;
import org.kohsuke.randname.RandomNameGenerator;
import java.io.FileInputStream;
import java.util.Properties;
import java.io.File;
/**
* @author Kohsuke Kawaguchi
@@ -17,16 +15,11 @@ public abstract class AbstractGitHubApiTestBase extends Assert {
@Before
public void setUp() throws Exception {
Properties props = new Properties();
java.io.File f = new java.io.File(System.getProperty("user.home"), ".github.kohsuke2");
File f = new File(System.getProperty("user.home"), ".github.kohsuke2");
if (f.exists()) {
FileInputStream in = new FileInputStream(f);
try {
props.load(in);
gitHub = GitHub.connect(props.getProperty("login"),props.getProperty("oauth"));
} finally {
IOUtils.closeQuietly(in);
}
// use the non-standard credential preferentially, so that developers of this library do not have
// to clutter their event stream.
gitHub = GitHubBuilder.fromPropertyFile(f.getPath()).build();
} else {
gitHub = GitHub.connect();
}

View File

@@ -3,6 +3,8 @@ package org.kohsuke.github;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import org.junit.Assume;
import org.junit.Test;
import org.kohsuke.github.GHCommit.File;
@@ -205,31 +207,31 @@ public class AppTest extends AbstractGitHubApiTestBase {
@Test
public void testMyTeamsContainsAllMyOrganizations() throws IOException {
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
Map<String, GHOrganization> myOrganizations = gitHub.getMyOrganizations();
assertEquals(teams.keySet(), myOrganizations.keySet());
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
Map<String, GHOrganization> myOrganizations = gitHub.getMyOrganizations();
assertEquals(teams.keySet(), myOrganizations.keySet());
}
@Test
public void testMyTeamsShouldIncludeMyself() throws IOException {
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
for (Entry<String, Set<GHTeam>> teamsPerOrg : teams.entrySet()) {
String organizationName = teamsPerOrg.getKey();
for (GHTeam team : teamsPerOrg.getValue()) {
String teamName = team.getName();
assertTrue("Team " + teamName + " in organization " + organizationName
+ " does not contain myself",
shouldBelongToTeam(organizationName, teamName));
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
for (Entry<String, Set<GHTeam>> teamsPerOrg : teams.entrySet()) {
String organizationName = teamsPerOrg.getKey();
for (GHTeam team : teamsPerOrg.getValue()) {
String teamName = team.getName();
assertTrue("Team " + teamName + " in organization " + organizationName
+ " does not contain myself",
shouldBelongToTeam(organizationName, teamName));
}
}
}
}
private boolean shouldBelongToTeam(String organizationName, String teamName) throws IOException {
GHOrganization org = gitHub.getOrganization(organizationName);
assertNotNull(org);
GHTeam team = org.getTeamByName(teamName);
assertNotNull(team);
return team.hasMember(gitHub.getMyself());
GHOrganization org = gitHub.getOrganization(organizationName);
assertNotNull(org);
GHTeam team = org.getTeamByName(teamName);
assertNotNull(team);
return team.hasMember(gitHub.getMyself());
}
@Test
@@ -665,6 +667,33 @@ public class AppTest extends AbstractGitHubApiTestBase {
assertEquals(readme.getName(),"README.md");
assertEquals(readme.getContent(),"This is a markdown readme.\n");
}
@Test
public void testTrees() throws IOException {
GHTree masterTree = gitHub.getRepository("kohsuke/github-api").getTree("master");
boolean foundReadme = false;
for(GHTreeEntry e : masterTree.getTree()){
if("readme".equalsIgnoreCase(e.getPath().replaceAll(".md", ""))){
foundReadme = true;
break;
}
}
assertTrue(foundReadme);
}
@Test
public void testTreesRecursive() throws IOException {
GHTree masterTree = gitHub.getRepository("kohsuke/github-api").getTreeRecursive("master", 1);
boolean foundThisFile = false;
for(GHTreeEntry e : masterTree.getTree()){
if(e.getPath().endsWith(AppTest.class.getSimpleName() + ".java")){
foundThisFile = true;
break;
}
}
assertTrue(foundThisFile);
}
@Test
public void testRepoLabel() throws IOException {
@@ -709,6 +738,45 @@ public class AppTest extends AbstractGitHubApiTestBase {
assertTrue(githubApi);
}
@Test
public void testListAllRepositories() throws Exception {
Iterator<GHRepository> itr = gitHub.listAllPublicRepositories().iterator();
for (int i=0; i<30; i++) {
assertTrue(itr.hasNext());
GHRepository r = itr.next();
System.out.println(r.getFullName());
assertNotNull(r.getUrl());
assertNotEquals(0,r.getId());
}
}
@Test // issue #162
public void testIssue162() throws Exception {
GHRepository r = gitHub.getRepository("kohsuke/github-api");
List<GHContent> contents = r.getDirectoryContent("", "gh-pages");
for (GHContent content : contents) {
if (content.isFile()) {
String content1 = content.getContent();
String content2 = r.getFileContent(content.getPath(), "gh-pages").getContent();
System.out.println(content.getPath());
assertEquals(content1, content2);
}
}
}
@Test
public void markDown() throws Exception {
assertEquals("<p><strong>Test日本語</strong></p>", IOUtils.toString(gitHub.renderMarkdown("**Test日本語**")).trim());
String actual = IOUtils.toString(gitHub.getRepository("kohsuke/github-api").renderMarkdown("@kohsuke to fix issue #1", MarkdownMode.GFM));
System.out.println(actual);
assertTrue(actual.contains("href=\"https://github.com/kohsuke\""));
assertTrue(actual.contains("href=\"https://github.com/kohsuke/github-api/pull/1\""));
assertTrue(actual.contains("class=\"user-mention\""));
assertTrue(actual.contains("class=\"issue-link\""));
assertTrue(actual.contains("to fix issue"));
}
private void kohsuke() {
String login = getUser().getLogin();
Assume.assumeTrue(login.equals("kohsuke") || login.equals("kohsuke2"));

View File

@@ -98,15 +98,16 @@ public class GitHubTest extends TestCase {
props.put("customLogin", "bogusLogin");
props.put("customOauth", "bogusOauth");
props.put("customPassword", "bogusPassword");
props.put("customEndpoint", "bogusEndpoint");
setupEnvironment(props);
GitHubBuilder builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth");
GitHubBuilder builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
assertEquals("bogusLogin", builder.user);
assertEquals("bogusOauth", builder.oauthToken);
assertEquals("bogusPassword", builder.password);
assertEquals("bogusEndpoint", builder.endpoint);
}
}

View File

@@ -43,4 +43,11 @@ public class RepositoryTest extends AbstractGitHubApiTestBase {
private GHRepository getRepository() throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("jenkins");
}
@Test
public void listLanguages() throws IOException {
GHRepository r = gitHub.getRepository("kohsuke/github-api");
String mainLanguage = r.getLanguage();
assertTrue(r.listLanguages().containsKey(mainLanguage));
}
}