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 {