diff --git a/src/main/java/org/kohsuke/github/GHContent.java b/src/main/java/org/kohsuke/github/GHContent.java
index 4900a19214..cd04e436f1 100644
--- a/src/main/java/org/kohsuke/github/GHContent.java
+++ b/src/main/java/org/kohsuke/github/GHContent.java
@@ -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 String getPath() {
/**
* Retrieve the decoded content that is stored at this location.
*
+ *
* 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.
*
+ *
* 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 String getHtmlUrl() {
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);
}
diff --git a/src/main/java/org/kohsuke/github/GHRateLimit.java b/src/main/java/org/kohsuke/github/GHRateLimit.java
index 9d54953d54..301bd7993d 100644
--- a/src/main/java/org/kohsuke/github/GHRateLimit.java
+++ b/src/main/java/org/kohsuke/github/GHRateLimit.java
@@ -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() +
+ '}';
}
}
diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java
index 8570a09bb1..1c9fcc7b34 100644
--- a/src/main/java/org/kohsuke/github/GHRepository.java
+++ b/src/main/java/org/kohsuke/github/GHRepository.java
@@ -30,7 +30,9 @@
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 @@ protected void wrapUp(GHTag[] page) {
};
}
+ /**
+ * 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 listLanguages() throws IOException {
+ return root.retrieve().to(getApiTailUrl("languages"), HashMap.class);
+ }
+
public String getOwnerName() {
return owner.login;
}
@@ -641,6 +655,35 @@ public GHRef getRef(String refName) throws IOException {
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 GHContent getFileContent(String path, String ref) throws IOException {
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 getDirectoryContent(String path) throws IOException {
@@ -1002,10 +1042,7 @@ public List getDirectoryContent(String path, String ref) throws IOExc
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 int getContributions() {
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
diff --git a/src/main/java/org/kohsuke/github/GHTree.java b/src/main/java/org/kohsuke/github/GHTree.java
new file mode 100644
index 0000000000..90cf3c78e8
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHTree.java
@@ -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 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;
+ }
+
+}
diff --git a/src/main/java/org/kohsuke/github/GHTreeEntry.java b/src/main/java/org/kohsuke/github/GHTreeEntry.java
new file mode 100644
index 0000000000..e3d831c073
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHTreeEntry.java
@@ -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);
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java
index c560227498..ec1a2a46d5 100644
--- a/src/main/java/org/kohsuke/github/GitHub.java
+++ b/src/main/java/org/kohsuke/github/GitHub.java
@@ -26,8 +26,10 @@
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 @@ protected GHUser getUser(GHUser orig) throws IOException {
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 Map getMyOrganizations() throws IOException {
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> getMyTeams() throws IOException {
- Map> allMyTeams = new HashMap>();
- for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) {
- team.wrapUp(this);
- String orgLogin = team.getOrganization().getLogin();
- Set teamsPerOrg = allMyTeams.get(orgLogin);
- if (teamsPerOrg == null) {
- teamsPerOrg = new HashSet();
- }
- teamsPerOrg.add(team);
- allMyTeams.put(orgLogin, teamsPerOrg);
- }
- return allMyTeams;
- }
+ /**
+ * 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> getMyTeams() throws IOException {
+ Map> allMyTeams = new HashMap>();
+ for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) {
+ team.wrapUp(this);
+ String orgLogin = team.getOrganization().getLogin();
+ Set teamsPerOrg = allMyTeams.get(orgLogin);
+ if (teamsPerOrg == null) {
+ teamsPerOrg = new HashSet();
+ }
+ teamsPerOrg.add(team);
+ allMyTeams.put(orgLogin, teamsPerOrg);
+ }
+ return allMyTeams;
+ }
/**
* Public events visible to you. Equivalent of what's displayed on https://github.com/
@@ -438,6 +443,53 @@ public GHIssueSearchBuilder searchIssues() {
return new GHIssueSearchBuilder(this);
}
+ /**
+ * This provides a dump of every public repository, in the order that they were created.
+ * @see documentation
+ */
+ public PagedIterable 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 you’ve seen. See {@link GHRepository#getId()}
+ * @see documentation
+ */
+ public PagedIterable listAllPublicRepositories(final String since) {
+ return new PagedIterable() {
+ public PagedIterator iterator() {
+ return new PagedIterator(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.
+ *
+ *
+ * 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 {
diff --git a/src/main/java/org/kohsuke/github/GitHubBuilder.java b/src/main/java/org/kohsuke/github/GitHubBuilder.java
index e794fb14ca..98248b94b0 100644
--- a/src/main/java/org/kohsuke/github/GitHubBuilder.java
+++ b/src/main/java/org/kohsuke/github/GitHubBuilder.java
@@ -9,102 +9,119 @@
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 {
-
-
+ 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();
-
- 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);
-
+ 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.
+ *
+ *
+ * The following environment variables are recognized:
+ *
+ *
+ * - GITHUB_LOGIN: username like 'kohsuke'
+ *
- GITHUB_PASSWORD: raw password
+ *
- GITHUB_OAUTH: OAuth token to login
+ *
- GITHUB_ENDPOINT: URL of the API endpoint
+ *
+ *
+ *
+ * See class javadoc for the relationship between these coordinates.
+ *
+ *
+ * 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 env = System.getenv();
-
- for (Map.Entry element : env.entrySet()) {
-
- props.put(element.getKey(), element.getValue());
- }
-
+ for (Entry 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 static GitHubBuilder fromPropertyFile() throws IOException {
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 static GitHubBuilder fromProperties(Properties props) {
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 GitHubBuilder withConnector(HttpConnector connector) {
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 HttpURLConnection connect(URL url) throws IOException {
}
public GitHub build() throws IOException {
- return new GitHub(endpoint, user, oauthToken, password, connector);
+ return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler);
}
}
diff --git a/src/main/java/org/kohsuke/github/MarkdownMode.java b/src/main/java/org/kohsuke/github/MarkdownMode.java
new file mode 100644
index 0000000000..1d63760555
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/MarkdownMode.java
@@ -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);
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/RateLimitHandler.java b/src/main/java/org/kohsuke/github/RateLimitHandler.java
new file mode 100644
index 0000000000..b00624ce2b
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/RateLimitHandler.java
@@ -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.
+ *
+ *
+ * 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 API documentation from GitHub
+ * @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);
+ }
+ };
+}
diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java
index e2b0362a4c..7f34e72144 100644
--- a/src/main/java/org/kohsuke/github/Requester.java
+++ b/src/main/java/org/kohsuke/github/Requester.java
@@ -29,7 +29,6 @@
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.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 @@ public T to(String tailApiUrl, Class type, String method) throws IOExcept
private T _to(String tailApiUrl, Class 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 @@ public int asHttpStatusCode(String tailApiUrl) throws IOException {
}
}
+ 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 @@ private T parse(HttpURLConnection uc, Class type, T instance) throws IOEx
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).readValue(data);
return null;
@@ -417,18 +441,11 @@ private InputStream wrapStream(HttpURLConnection uc, InputStream in) throws IOEx
}
/**
- * 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)
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index 3956df708a..3c2af01970 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -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
-----
diff --git a/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java b/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java
index e45895d66e..a4311429d7 100644
--- a/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java
+++ b/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java
@@ -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();
}
diff --git a/src/test/java/org/kohsuke/github/AppTest.java b/src/test/java/org/kohsuke/github/AppTest.java
index a13f5423f5..6e56bbff89 100755
--- a/src/test/java/org/kohsuke/github/AppTest.java
+++ b/src/test/java/org/kohsuke/github/AppTest.java
@@ -3,6 +3,8 @@
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 void testMyOrganizations() throws IOException {
@Test
public void testMyTeamsContainsAllMyOrganizations() throws IOException {
- Map> teams = gitHub.getMyTeams();
- Map myOrganizations = gitHub.getMyOrganizations();
- assertEquals(teams.keySet(), myOrganizations.keySet());
+ Map> teams = gitHub.getMyTeams();
+ Map myOrganizations = gitHub.getMyOrganizations();
+ assertEquals(teams.keySet(), myOrganizations.keySet());
}
@Test
public void testMyTeamsShouldIncludeMyself() throws IOException {
- Map> teams = gitHub.getMyTeams();
- for (Entry> 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> teams = gitHub.getMyTeams();
+ for (Entry> 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 void testReadme() throws IOException {
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 void testSubscribers() throws IOException {
assertTrue(githubApi);
}
+ @Test
+ public void testListAllRepositories() throws Exception {
+ Iterator 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 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("Test日本語
", 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"));
diff --git a/src/test/java/org/kohsuke/github/GitHubTest.java b/src/test/java/org/kohsuke/github/GitHubTest.java
index 3e876f0b10..4ac4ecfea2 100644
--- a/src/test/java/org/kohsuke/github/GitHubTest.java
+++ b/src/test/java/org/kohsuke/github/GitHubTest.java
@@ -98,15 +98,16 @@ public void testGitHubBuilderFromCustomEnvironment() throws IOException {
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);
}
}
diff --git a/src/test/java/org/kohsuke/github/RepositoryTest.java b/src/test/java/org/kohsuke/github/RepositoryTest.java
index 6862ace7be..61cdc09dca 100644
--- a/src/test/java/org/kohsuke/github/RepositoryTest.java
+++ b/src/test/java/org/kohsuke/github/RepositoryTest.java
@@ -43,4 +43,11 @@ public void listContributors() throws IOException {
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));
+ }
}