diff --git a/pom.xml b/pom.xml index 433cb6bdd0..ec7557ffb0 100644 --- a/pom.xml +++ b/pom.xml @@ -3,11 +3,11 @@ org.kohsuke pom - 12 + 14 github-api - 1.63 + 1.64-SNAPSHOT GitHub API for Java http://github-api.kohsuke.org/ GitHub API for Java @@ -16,7 +16,7 @@ scm:git:git@github.com/kohsuke/${project.artifactId}.git scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git http://${project.artifactId}.kohsuke.org/ - github-api-1.63 + HEAD 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: + * + *

+ * + *

+ * 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)); + } }