diff --git a/pom.xml b/pom.xml index 6650872541..4847f4885c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.70 + 1.71 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.70 + github-api-1.71 @@ -28,7 +28,7 @@ UTF-8 - 3.0.1 + 3.0.2 true @@ -52,7 +52,6 @@ ${findbugs-maven-plugin.version} true - true ${findbugs-maven-plugin.failOnError} diff --git a/src/main/java/org/kohsuke/github/GHHooks.java b/src/main/java/org/kohsuke/github/GHHooks.java index 1a6154aa6d..4ee939c5a8 100644 --- a/src/main/java/org/kohsuke/github/GHHooks.java +++ b/src/main/java/org/kohsuke/github/GHHooks.java @@ -21,8 +21,9 @@ private Context(GitHub root) { } public List getHooks() throws IOException { - List list = new ArrayList(Arrays.asList( - root.retrieve().to(collection(), collectionClass()))); + + GHHook [] hookArray = root.retrieve().to(collection(),collectionClass()); // jdk/eclipse bug requires this to be on separate line + List list = new ArrayList(Arrays.asList(hookArray)); for (GHHook h : list) wrap(h); return list; diff --git a/src/main/java/org/kohsuke/github/GHMilestone.java b/src/main/java/org/kohsuke/github/GHMilestone.java index 146c92abb3..0d1b43224e 100644 --- a/src/main/java/org/kohsuke/github/GHMilestone.java +++ b/src/main/java/org/kohsuke/github/GHMilestone.java @@ -17,6 +17,7 @@ public class GHMilestone extends GHObject { GHUser creator; private String state, due_on, title, description, html_url; private int closed_issues, open_issues, number; + protected String closed_at; public GitHub getRoot() { return root; @@ -34,7 +35,14 @@ public Date getDueOn() { if (due_on == null) return null; return GitHub.parseDate(due_on); } - + + /** + * When was this milestone closed? + */ + public Date getClosedAt() throws IOException { + return GitHub.parseDate(closed_at); + } + public String getTitle() { return title; } diff --git a/src/main/java/org/kohsuke/github/GHObject.java b/src/main/java/org/kohsuke/github/GHObject.java index 50aa0a6a86..270ac1fcc5 100644 --- a/src/main/java/org/kohsuke/github/GHObject.java +++ b/src/main/java/org/kohsuke/github/GHObject.java @@ -29,6 +29,11 @@ public Date getCreatedAt() throws IOException { return GitHub.parseDate(created_at); } + @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification = "Bridge method of getCreatedAt") + private Object createdAtStr(Date id, Class type) { + return created_at; + } + /** * API URL of this object. */ @@ -57,4 +62,14 @@ public Date getUpdatedAt() throws IOException { public int getId() { return id; } + + @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification = "Bridge method of getId") + private Object intToString(int id, Class type) { + return String.valueOf(id); + } + + @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification = "Bridge method of getHtmlUrl") + private Object urlToString(URL url, Class type) { + return url==null ? null : url.toString(); + } } diff --git a/src/main/java/org/kohsuke/github/GHPullRequest.java b/src/main/java/org/kohsuke/github/GHPullRequest.java index 909110b727..5604d2355e 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequest.java +++ b/src/main/java/org/kohsuke/github/GHPullRequest.java @@ -50,6 +50,7 @@ public class GHPullRequest extends GHIssue { private int deletions; private String mergeable_state; private int changed_files; + private String merge_commit_sha; /** * GitHub doesn't return some properties of {@link GHIssue} when requesting the GET on the 'pulls' API @@ -142,9 +143,9 @@ public PullRequest getPullRequest() { } // -// details that are only available via get with ID -// -// + // details that are only available via get with ID + // + public GHUser getMergedBy() throws IOException { populate(); return merged_by; @@ -185,6 +186,14 @@ public int getChangedFiles() throws IOException { return changed_files; } + /** + * See GitHub blog post + */ + public String getMergeCommitSha() throws IOException { + populate(); + return merge_commit_sha; + } + /** * Fully populate the data by retrieving missing data. * diff --git a/src/main/java/org/kohsuke/github/GHRelease.java b/src/main/java/org/kohsuke/github/GHRelease.java index 901bb8f359..b61dc62b38 100644 --- a/src/main/java/org/kohsuke/github/GHRelease.java +++ b/src/main/java/org/kohsuke/github/GHRelease.java @@ -129,14 +129,9 @@ public GHAsset uploadAsset(File file, String contentType) throws IOException { String url = format("https://uploads.github.com%s/releases/%d/assets?name=%s", owner.getApiTailUrl(""), getId(), file.getName()); - FileInputStream istream = new FileInputStream(file); - try { - return builder.contentType(contentType) - .with(istream) + return builder.contentType(contentType) + .with(new FileInputStream(file)) .to(url, GHAsset.class).wrap(this); - } finally { - istream.close(); - } } public List getAssets() throws IOException { diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 54acc2c8c3..917be54d7f 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -54,7 +54,7 @@ public class GHRepository extends GHObject { private String description, homepage, name, full_name; private String html_url; // this is the UI - private String git_url, ssh_url, clone_url, svn_url; + private String git_url, ssh_url, clone_url, svn_url, mirror_url; private GHUser owner; // not fully populated. beware. private boolean has_issues, has_wiki, fork, has_downloads; @JsonProperty("private") @@ -159,6 +159,14 @@ public String getSvnUrl() { return svn_url; } + /** + * Gets the Mirror URL to access this repository: https://github.com/apache/tomee + * mirrored from git://git.apache.org/tomee.git + */ + public String getMirrorUrl() { + return mirror_url; + } + /** * Gets the SSH URL to access this repository, such as git@github.com:rails/rails.git */ @@ -682,7 +690,23 @@ public GHCompare getCompare(GHCommit id1, GHCommit id2) throws IOException { } public GHCompare getCompare(GHBranch id1, GHBranch id2) throws IOException { - return getCompare(id1.getName(),id2.getName()); + + GHRepository owner1 = id1.getOwner(); + GHRepository owner2 = id2.getOwner(); + + // If the owner of the branches is different, we have a cross-fork compare. + if (owner1!=null && owner2!=null) { + String ownerName1 = owner1.getOwnerName(); + String ownerName2 = owner2.getOwnerName(); + if (!StringUtils.equals(ownerName1, ownerName2)) { + String qualifiedName1 = String.format("%s:%s", ownerName1, id1.getName()); + String qualifiedName2 = String.format("%s:%s", ownerName2, id2.getName()); + return getCompare(qualifiedName1, qualifiedName2); + } + } + + return getCompare(id1.getName(), id2.getName()); + } /** @@ -1094,6 +1118,9 @@ public List getDirectoryContent(String path) throws IOException { public List getDirectoryContent(String path, String ref) throws IOException { Requester requester = root.retrieve(); + while (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } String target = getApiTailUrl("contents/" + path); GHContent[] files = requester.with("ref",ref).to(target, GHContent[].class); diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 9e638bb4aa..2ef822680c 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -47,6 +47,7 @@ import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import org.apache.commons.codec.Charsets; import org.apache.commons.codec.binary.Base64; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -129,12 +130,7 @@ public class GitHub { } else { if (password!=null) { String authorization = (login + ':' + password); - final Charset charset; - try { - charset = Charset.forName("UTF-8"); - } catch (Exception ex) { - throw new IOException("UTF-8 encoding is not supported", ex); - } + Charset charset = Charsets.UTF_8; encodedAuthorization = "Basic "+new String(Base64.encodeBase64(authorization.getBytes(charset)), charset); } else {// anonymous access encodedAuthorization = null; @@ -204,6 +200,15 @@ public static GitHub connectAnonymously() throws IOException { return new GitHubBuilder().build(); } + /** + * Connects to GitHub Enterprise anonymously. + * + * All operations that requires authentication will fail. + */ + public static GitHub connectToEnterpriseAnonymously(String apiUrl) throws IOException { + return new GitHubBuilder().withEndpoint(apiUrl).build(); + } + /** * Is this an anonymous connection * @return {@code true} if operations that require authentication will fail. @@ -446,6 +451,29 @@ public boolean isCredentialValid() throws IOException { } } + private static class GHApiInfo { + private String rate_limit_url; + + void check(String apiUrl) throws IOException { + if (rate_limit_url==null) + throw new IOException(apiUrl+" doesn't look like GitHub API URL"); + + // make sure that the URL is legitimate + new URL(rate_limit_url); + } + } + + /** + * Ensures that the API URL is valid. + * + *

+ * This method returns normally if the endpoint is reachable and verified to be GitHub API URL. + * Otherwise this method throws {@link IOException} to indicate the problem. + */ + public void checkApiUrlValidity() throws IOException { + retrieve().to("/", GHApiInfo.class).check(apiUrl); + } + /** * Search issues. */ diff --git a/src/main/java/org/kohsuke/github/GitHubBuilder.java b/src/main/java/org/kohsuke/github/GitHubBuilder.java index c0ecaffda5..42a0f2be6f 100644 --- a/src/main/java/org/kohsuke/github/GitHubBuilder.java +++ b/src/main/java/org/kohsuke/github/GitHubBuilder.java @@ -1,6 +1,7 @@ package org.kohsuke.github; import org.apache.commons.io.IOUtils; +import org.kohsuke.github.extras.ImpatientHttpConnector; import java.io.File; import java.io.FileInputStream; @@ -51,7 +52,7 @@ public static GitHubBuilder fromCredentials() throws IOException { try { builder = fromPropertyFile(); - if (builder.user != null) + if (builder.oauthToken != null || builder.user != null) return builder; } catch (FileNotFoundException e) { // fall through @@ -60,7 +61,7 @@ public static GitHubBuilder fromCredentials() throws IOException { builder = fromEnvironment(); - if (builder.user != null) + if (builder.oauthToken != null || builder.user != null) return builder; else throw (IOException)new IOException("Failed to resolve credentials from ~/.github or the environment.").initCause(cause); @@ -184,11 +185,11 @@ public GitHubBuilder withRateLimitHandler(RateLimitHandler handler) { * the system default one. */ public GitHubBuilder withProxy(final Proxy p) { - return withConnector(new HttpConnector() { + return withConnector(new ImpatientHttpConnector(new HttpConnector() { public HttpURLConnection connect(URL url) throws IOException { return (HttpURLConnection) url.openConnection(p); } - }); + })); } public GitHub build() throws IOException { diff --git a/src/main/java/org/kohsuke/github/HttpConnector.java b/src/main/java/org/kohsuke/github/HttpConnector.java index 6cff72dc53..5496268561 100644 --- a/src/main/java/org/kohsuke/github/HttpConnector.java +++ b/src/main/java/org/kohsuke/github/HttpConnector.java @@ -1,8 +1,11 @@ package org.kohsuke.github; +import org.kohsuke.github.extras.ImpatientHttpConnector; + import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; +import java.util.concurrent.TimeUnit; /** * Pluggability for customizing HTTP request behaviors or using altogether different library. @@ -21,9 +24,9 @@ public interface HttpConnector { /** * Default implementation that uses {@link URL#openConnection()}. */ - HttpConnector DEFAULT = new HttpConnector() { + HttpConnector DEFAULT = new ImpatientHttpConnector(new HttpConnector() { public HttpURLConnection connect(URL url) throws IOException { return (HttpURLConnection) url.openConnection(); } - }; + }); } diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 78ead072d9..81fe507d8b 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -53,6 +53,8 @@ import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; +import javax.annotation.WillClose; + import static java.util.Arrays.asList; import static org.kohsuke.github.GitHub.*; @@ -143,7 +145,7 @@ public Requester with(String key, Map value) { return _with(key, value); } - public Requester with(InputStream body) { + public Requester with(@WillClose/*later*/ InputStream body) { this.body = body; return this; } @@ -257,6 +259,8 @@ public int asHttpStatusCode(String tailApiUrl) throws IOException { while (true) {// loop while API rate limit is hit setupConnection(root.getApiURL(tailApiUrl)); + uc.setRequestMethod("GET"); + buildRequest(); try { @@ -271,8 +275,11 @@ public InputStream asStream(String tailApiUrl) throws IOException { while (true) {// loop while API rate limit is hit setupConnection(root.getApiURL(tailApiUrl)); - buildRequest(); - + // if the download link is encoded with a token on the query string, the default behavior of POST will fail + uc.setRequestMethod("GET"); + + buildRequest(); + try { return wrapStream(uc.getInputStream()); } catch (IOException e) { diff --git a/src/main/java/org/kohsuke/github/extras/ImpatientHttpConnector.java b/src/main/java/org/kohsuke/github/extras/ImpatientHttpConnector.java new file mode 100644 index 0000000000..740b9a037d --- /dev/null +++ b/src/main/java/org/kohsuke/github/extras/ImpatientHttpConnector.java @@ -0,0 +1,58 @@ +package org.kohsuke.github.extras; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.kohsuke.github.HttpConnector; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +/** + * {@link HttpConnector} wrapper that sets timeout + * + * @author Kohsuke Kawaguchi + */ +public class ImpatientHttpConnector implements HttpConnector { + private final HttpConnector base; + private final int readTimeout, connectTimeout; + + /** + * @param connectTimeout + * HTTP connection timeout in milliseconds + * @param readTimeout + * HTTP read timeout in milliseconds + */ + public ImpatientHttpConnector(HttpConnector base, int connectTimeout, int readTimeout) { + this.base = base; + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; + } + + public ImpatientHttpConnector(HttpConnector base, int timeout) { + this(base,timeout,timeout); + } + + public ImpatientHttpConnector(HttpConnector base) { + this(base,CONNECT_TIMEOUT,READ_TIMEOUT); + } + + public HttpURLConnection connect(URL url) throws IOException { + HttpURLConnection con = base.connect(url); + con.setConnectTimeout(connectTimeout); + con.setReadTimeout(readTimeout); + return con; + } + + /** + * Default connection timeout in milliseconds + */ + @SuppressFBWarnings("MS_SHOULD_BE_FINAL") + public static int CONNECT_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(10); + + /** + * Default read timeout in milliseconds + */ + @SuppressFBWarnings("MS_SHOULD_BE_FINAL") + public static int READ_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(10); +} diff --git a/src/test/java/org/kohsuke/github/AppTest.java b/src/test/java/org/kohsuke/github/AppTest.java index d2b68e112c..5d5e0a5fef 100755 --- a/src/test/java/org/kohsuke/github/AppTest.java +++ b/src/test/java/org/kohsuke/github/AppTest.java @@ -286,7 +286,8 @@ public void testOrgFork() throws Exception { @Test public void testGetTeamsForRepo() throws Exception { kohsuke(); - assertEquals(1, gitHub.getOrganization("github-api-test-org").getRepository("testGetTeamsForRepo").getTeams().size()); + // 'Core Developers' and 'Owners' + assertEquals(2, gitHub.getOrganization("github-api-test-org").getRepository("testGetTeamsForRepo").getTeams().size()); } @Test diff --git a/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java b/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java index 39f8545aaa..7848143a86 100644 --- a/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java +++ b/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java @@ -43,6 +43,14 @@ public void testGetDirectoryContent() throws Exception { assertTrue(entries.size() == 3); } + @Test + public void testGetDirectoryContentTrailingSlash() throws Exception { + //Used to truncate the ?ref=master, see gh-224 https://github.com/kohsuke/github-api/pull/224 + List entries = repo.getDirectoryContent("ghcontent-ro/a-dir-with-3-entries/", "master"); + + assertTrue(entries.get(0).getUrl().endsWith("?ref=master")); + } + @Test public void testCRUDContent() throws Exception { GHContentUpdateResponse created = repo.createContent("this is an awesome file I created\n", "Creating a file for integration tests.", createdFilename); diff --git a/src/test/java/org/kohsuke/github/GitHubTest.java b/src/test/java/org/kohsuke/github/GitHubTest.java index 54ce6e35d7..1663d24144 100644 --- a/src/test/java/org/kohsuke/github/GitHubTest.java +++ b/src/test/java/org/kohsuke/github/GitHubTest.java @@ -12,6 +12,7 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -119,4 +120,10 @@ public void testGitHubEnterpriseDoesNotHaveRateLimit() throws IOException { GHRateLimit rateLimit = github.getRateLimit(); assertThat(rateLimit.getResetDate(), notNullValue()); } + + @Test + public void testGitHubIsApiUrlValid() throws IOException { + GitHub github = GitHub.connectAnonymously(); + github.checkApiUrlValidity(); + } } diff --git a/src/test/java/org/kohsuke/github/PullRequestTest.java b/src/test/java/org/kohsuke/github/PullRequestTest.java index 9fc55f7519..c7ce77d289 100644 --- a/src/test/java/org/kohsuke/github/PullRequestTest.java +++ b/src/test/java/org/kohsuke/github/PullRequestTest.java @@ -49,6 +49,26 @@ public void testPullRequestReviewComments() throws Exception { assertTrue(comments.isEmpty()); } + @Test + public void testMergeCommitSHA() throws Exception { + String name = rnd.next(); + GHPullRequest p = getRepository().createPullRequest(name, "mergeable-branch", "master", "## test"); + for (int i=0; i<100; i++) { + GHPullRequest updated = getRepository().getPullRequest(p.getNumber()); + if (updated.getMergeCommitSha()!=null) { + // make sure commit exists + GHCommit commit = getRepository().getCommit(updated.getMergeCommitSha()); + assertNotNull(commit); + return; + } + + // mergeability computation takes time. give it more chance + Thread.sleep(100); + } + // hmm? + fail(); + } + @Test // Requires push access to the test repo to pass public void setLabels() throws Exception {