getCollaboratorNames() throws IOException {
return r;
}
+ /**
+ * Obtain permission for a given user in this repository.
+ * @param user a {@link GHUser#getLogin}
+ * @throws FileNotFoundException under some conditions (e.g., private repo you can see but are not an admin of); treat as unknown
+ * @throws HttpException with a 403 under other conditions (e.g., public repo you have no special rights to); treat as unknown
+ */
+ @Deprecated @Preview
+ public GHPermissionType getPermission(String user) throws IOException {
+ GHPermission perm = root.retrieve().withPreview(KORRA).to(getApiTailUrl("collaborators/" + user + "/permission"), GHPermission.class);
+ perm.wrapUp(root);
+ return perm.getPermissionType();
+ }
+
+ /**
+ * Obtain permission for a given user in this repository.
+ * @throws FileNotFoundException under some conditions (e.g., private repo you can see but are not an admin of); treat as unknown
+ * @throws HttpException with a 403 under other conditions (e.g., public repo you have no special rights to); treat as unknown
+ */
+ @Deprecated @Preview
+ public GHPermissionType getPermission(GHUser u) throws IOException {
+ return getPermission(u.getLogin());
+ }
+
/**
* If this repository belongs to an organization, return a set of teams.
*/
@@ -818,7 +840,7 @@ public GHTree getTreeRecursive(String sha, int recursive) throws IOException {
}
/**
- * Obtains the metadata & the content of a blob.
+ * Obtains the metadata & the content of a blob.
*
*
* This method retrieves the whole content in memory, so beware when you are dealing with large BLOB.
diff --git a/src/main/java/org/kohsuke/github/GHTree.java b/src/main/java/org/kohsuke/github/GHTree.java
index 59c7cd287e..d52e02a793 100644
--- a/src/main/java/org/kohsuke/github/GHTree.java
+++ b/src/main/java/org/kohsuke/github/GHTree.java
@@ -30,7 +30,6 @@ public String getSha() {
/**
* Return an array of entries of the trees
- * @return
*/
public List getTree() {
return Collections.unmodifiableList(Arrays.asList(tree));
diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java
index b50b2234ea..df9353e032 100644
--- a/src/main/java/org/kohsuke/github/GitHub.java
+++ b/src/main/java/org/kohsuke/github/GitHub.java
@@ -23,12 +23,10 @@
*/
package org.kohsuke.github;
-import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
-import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
-import static java.util.logging.Level.FINE;
-import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
-import static org.kohsuke.github.Previews.DRAX;
-
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std;
+import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -49,17 +47,19 @@
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
-
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
import org.apache.commons.codec.Charsets;
import org.apache.commons.codec.binary.Base64;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std;
-import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
-
-import javax.annotation.Nonnull;
-import java.util.logging.Logger;
+import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
+import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static java.util.logging.Level.FINE;
+import static org.kohsuke.github.Previews.DRAX;
/**
* Root of the GitHub API.
@@ -90,6 +90,10 @@ public class GitHub {
private HttpConnector connector = HttpConnector.DEFAULT;
+ private final Object headerRateLimitLock = new Object();
+ private GHRateLimit headerRateLimit = null;
+ private volatile GHRateLimit rateLimit = null;
+
/**
* Creates a client API root object.
*
@@ -254,6 +258,10 @@ public HttpConnector getConnector() {
return connector;
}
+ public String getApiUrl() {
+ return apiUrl;
+ }
+
/**
* Sets the custom connector used to make requests to GitHub.
*/
@@ -287,17 +295,61 @@ public void setConnector(HttpConnector connector) {
*/
public GHRateLimit getRateLimit() throws IOException {
try {
- return retrieve().to("/rate_limit", JsonRateLimit.class).rate;
+ return rateLimit = retrieve().to("/rate_limit", JsonRateLimit.class).rate;
} catch (FileNotFoundException e) {
// GitHub Enterprise doesn't have the rate limit, so in that case
// return some big number that's not too big.
// see issue #78
GHRateLimit r = new GHRateLimit();
r.limit = r.remaining = 1000000;
- long hours = 1000L * 60 * 60;
- r.reset = new Date(System.currentTimeMillis() + 1 * hours );
- return r;
+ long hour = 60L * 60L; // this is madness, storing the date as seconds in a Date object
+ r.reset = new Date((System.currentTimeMillis() + hour) / 1000L );
+ return rateLimit = r;
+ }
+ }
+
+ /*package*/ void updateRateLimit(@Nonnull GHRateLimit observed) {
+ synchronized (headerRateLimitLock) {
+ if (headerRateLimit == null
+ || headerRateLimit.getResetDate().getTime() < observed.getResetDate().getTime()
+ || headerRateLimit.remaining > observed.remaining) {
+ headerRateLimit = observed;
+ LOGGER.log(Level.INFO, "Rate limit now: {0}", headerRateLimit);
+ }
+ }
+ }
+
+ /**
+ * Returns the most recently observed rate limit data or {@code null} if either there is no rate limit
+ * (for example GitHub Enterprise) or if no requests have been made.
+ *
+ * @return the most recently observed rate limit data or {@code null}.
+ */
+ @CheckForNull
+ public GHRateLimit lastRateLimit() {
+ synchronized (headerRateLimitLock) {
+ return headerRateLimit;
+ }
+ }
+
+ /**
+ * Gets the current rate limit while trying not to actually make any remote requests unless absolutely necessary.
+ *
+ * @return the current rate limit data.
+ * @throws IOException if we couldn't get the current rate limit data.
+ */
+ @Nonnull
+ public GHRateLimit rateLimit() throws IOException {
+ synchronized (headerRateLimitLock) {
+ if (headerRateLimit != null) {
+ return headerRateLimit;
+ }
+ }
+ GHRateLimit rateLimit = this.rateLimit;
+ if (rateLimit == null || rateLimit.getResetDate().getTime() < System.currentTimeMillis()) {
+ rateLimit = getRateLimit();
}
+ return rateLimit;
}
/**
@@ -416,7 +468,6 @@ protected void wrapUp(GHUser[] page) {
*
* @param key The license key provided from the API
* @return The license details
- * @throws IOException
* @see GHLicense#getKey()
*/
@Preview @Deprecated
diff --git a/src/main/java/org/kohsuke/github/Previews.java b/src/main/java/org/kohsuke/github/Previews.java
index f95a28b42b..238b062b8b 100644
--- a/src/main/java/org/kohsuke/github/Previews.java
+++ b/src/main/java/org/kohsuke/github/Previews.java
@@ -7,4 +7,5 @@
static final String LOKI = "application/vnd.github.loki-preview+json";
static final String DRAX = "application/vnd.github.drax-preview+json";
static final String SQUIRREL_GIRL = "application/vnd.github.squirrel-girl-preview";
+ static final String KORRA = "application/vnd.github.korra-preview";
}
diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java
index 70c48940c0..633e236dec 100644
--- a/src/main/java/org/kohsuke/github/Requester.java
+++ b/src/main/java/org/kohsuke/github/Requester.java
@@ -25,8 +25,6 @@
import com.fasterxml.jackson.databind.JsonMappingException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import org.apache.commons.io.IOUtils;
-
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -42,6 +40,7 @@
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -49,16 +48,18 @@
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
-
import javax.annotation.WillClose;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.StringUtils;
import static java.util.Arrays.asList;
import static java.util.logging.Level.FINE;
-import static org.kohsuke.github.GitHub.*;
+import static org.kohsuke.github.GitHub.MAPPER;
/**
* A builder pattern for making HTTP call and parsing its output.
@@ -281,6 +282,8 @@ private T _to(String tailApiUrl, Class type, T instance) throws IOExcepti
return result;
} catch (IOException e) {
handleApiError(e);
+ } finally {
+ noteRateLimit(tailApiUrl);
}
}
}
@@ -299,6 +302,8 @@ public int asHttpStatusCode(String tailApiUrl) throws IOException {
return uc.getResponseCode();
} catch (IOException e) {
handleApiError(e);
+ } finally {
+ noteRateLimit(tailApiUrl);
}
}
}
@@ -313,6 +318,59 @@ public InputStream asStream(String tailApiUrl) throws IOException {
return wrapStream(uc.getInputStream());
} catch (IOException e) {
handleApiError(e);
+ } finally {
+ noteRateLimit(tailApiUrl);
+ }
+ }
+ }
+
+ private void noteRateLimit(String tailApiUrl) {
+ if ("/rate_limit".equals(tailApiUrl)) {
+ // the rate_limit API is "free"
+ return;
+ }
+ if (tailApiUrl.startsWith("/search")) {
+ // the search API uses a different rate limit
+ return;
+ }
+ String limit = uc.getHeaderField("X-RateLimit-Limit");
+ if (StringUtils.isBlank(limit)) {
+ // if we are missing a header, return fast
+ return;
+ }
+ String remaining = uc.getHeaderField("X-RateLimit-Remaining");
+ if (StringUtils.isBlank(remaining)) {
+ // if we are missing a header, return fast
+ return;
+ }
+ String reset = uc.getHeaderField("X-RateLimit-Reset");
+ if (StringUtils.isBlank(reset)) {
+ // if we are missing a header, return fast
+ return;
+ }
+ GHRateLimit observed = new GHRateLimit();
+ try {
+ observed.limit = Integer.parseInt(limit);
+ } catch (NumberFormatException e) {
+ if (LOGGER.isLoggable(Level.FINEST)) {
+ LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Limit header value " + limit, e);
+ }
+ return;
+ }
+ try {
+ observed.remaining = Integer.parseInt(remaining);
+ } catch (NumberFormatException e) {
+ if (LOGGER.isLoggable(Level.FINEST)) {
+ LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Remaining header value " + remaining, e);
+ }
+ return;
+ }
+ try {
+ observed.reset = new Date(Long.parseLong(reset)); // this is madness, storing the date as seconds
+ root.updateRateLimit(observed);
+ } catch (NumberFormatException e) {
+ if (LOGGER.isLoggable(Level.FINEST)) {
+ LOGGER.log(Level.FINEST, "Malformed X-RateLimit-Reset header value " + reset, e);
}
}
}
@@ -382,7 +440,7 @@ private boolean isMethodWithBody() {
}
try {
- return new PagingIterator(type, root.getApiURL(s.toString()));
+ return new PagingIterator(type, tailApiUrl, root.getApiURL(s.toString()));
} catch (IOException e) {
throw new Error(e);
}
@@ -391,6 +449,7 @@ private boolean isMethodWithBody() {
class PagingIterator implements Iterator {
private final Class type;
+ private final String tailApiUrl;
/**
* The next batch to be returned from {@link #next()}.
@@ -402,9 +461,10 @@ class PagingIterator implements Iterator {
*/
private URL url;
- PagingIterator(Class type, URL url) {
- this.url = url;
+ PagingIterator(Class type, String tailApiUrl, URL url) {
this.type = type;
+ this.tailApiUrl = tailApiUrl;
+ this.url = url;
}
public boolean hasNext() {
@@ -438,6 +498,8 @@ private void fetch() {
return;
} catch (IOException e) {
handleApiError(e);
+ } finally {
+ noteRateLimit(tailApiUrl);
}
}
} catch (IOException e) {
@@ -598,11 +660,16 @@ private InputStream wrapStream(InputStream in) throws IOException {
InputStream es = wrapStream(uc.getErrorStream());
try {
if (es!=null) {
+ String error = IOUtils.toString(es, "UTF-8");
if (e instanceof FileNotFoundException) {
// pass through 404 Not Found to allow the caller to handle it intelligently
- throw (IOException) new FileNotFoundException(IOUtils.toString(es, "UTF-8")).initCause(e);
- } else
- throw (IOException) new IOException(IOUtils.toString(es, "UTF-8")).initCause(e);
+ throw (IOException) new FileNotFoundException(error).initCause(e);
+ } else if (e instanceof HttpException) {
+ HttpException http = (HttpException) e;
+ throw (IOException) new HttpException(error, http.getResponseCode(), http.getResponseMessage(), http.getUrl(), e);
+ } else {
+ throw (IOException) new IOException(error).initCause(e);
+ }
} else
throw e;
} finally {
diff --git a/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java b/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java
index f1753481e7..f32087c0d1 100644
--- a/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java
+++ b/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java
@@ -1,10 +1,12 @@
package org.kohsuke.github;
import org.junit.Assert;
+import org.junit.Assume;
import org.junit.Before;
import org.kohsuke.randname.RandomNameGenerator;
import java.io.File;
+import java.io.IOException;
/**
* @author Kohsuke Kawaguchi
@@ -25,5 +27,18 @@ public void setUp() throws Exception {
}
}
+ protected GHUser getUser() {
+ try {
+ return gitHub.getMyself();
+ } catch (IOException e) {
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ }
+
+ protected void kohsuke() {
+ String login = getUser().getLogin();
+ Assume.assumeTrue(login.equals("kohsuke") || login.equals("kohsuke2"));
+ }
+
protected static final RandomNameGenerator rnd = new RandomNameGenerator();
}
diff --git a/src/test/java/org/kohsuke/github/AppTest.java b/src/test/java/org/kohsuke/github/AppTest.java
index e1d513d739..a20b2eccf3 100755
--- a/src/test/java/org/kohsuke/github/AppTest.java
+++ b/src/test/java/org/kohsuke/github/AppTest.java
@@ -173,14 +173,6 @@ private GHRepository getTestRepository() throws IOException {
return repository;
}
- private GHUser getUser() {
- try {
- return gitHub.getMyself();
- } catch (IOException e) {
- throw new RuntimeException(e.getMessage(), e);
- }
- }
-
@Test
public void testListIssues() throws IOException {
GHUser u = getUser();
@@ -928,9 +920,4 @@ private void assertBlobContent(InputStream is) throws Exception {
assertThat(content,containsString("FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR"));
assertThat(content.length(),is(1104));
}
-
- private void kohsuke() {
- String login = getUser().getLogin();
- Assume.assumeTrue(login.equals("kohsuke") || login.equals("kohsuke2"));
- }
}
diff --git a/src/test/java/org/kohsuke/github/RepositoryTest.java b/src/test/java/org/kohsuke/github/RepositoryTest.java
index dae2892a45..9955c34956 100644
--- a/src/test/java/org/kohsuke/github/RepositoryTest.java
+++ b/src/test/java/org/kohsuke/github/RepositoryTest.java
@@ -3,6 +3,7 @@
import org.junit.Test;
import org.kohsuke.github.GHRepository.Contributor;
+import java.io.FileNotFoundException;
import java.io.IOException;
/**
@@ -40,6 +41,33 @@ public void listContributors() throws IOException {
assertTrue(kohsuke);
}
+ @Test
+ public void getPermission() throws Exception {
+ kohsuke();
+ GHRepository r = gitHub.getRepository("github-api-test-org/test-permission");
+ assertEquals(GHPermissionType.ADMIN, r.getPermission("kohsuke"));
+ assertEquals(GHPermissionType.READ, r.getPermission("dude"));
+ r = gitHub.getOrganization("apache").getRepository("groovy");
+ try {
+ r.getPermission("jglick");
+ fail();
+ } catch (HttpException x) {
+ x.printStackTrace(); // good
+ assertEquals(403, x.getResponseCode());
+ }
+
+ if (false) {
+ // can't easily test this; there's no private repository visible to the test user
+ r = gitHub.getOrganization("cloudbees").getRepository("private-repo-not-writable-by-me");
+ try {
+ r.getPermission("jglick");
+ fail();
+ } catch (FileNotFoundException x) {
+ x.printStackTrace(); // good
+ }
+ }
+ }
+
private GHRepository getRepository() throws IOException {
return gitHub.getOrganization("github-api-test-org").getRepository("jenkins");
}