From ccb42d32499ee1fcfe7dc9711f5e82eb5a9176b2 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Fri, 16 Dec 2016 18:02:28 -0500 Subject: [PATCH 01/12] [JENKINS-36240] Added GHRepository.getPermission(String). --- .../java/org/kohsuke/github/GHPermission.java | 53 +++++++++++++++++++ .../java/org/kohsuke/github/GHRepository.java | 12 +++++ .../java/org/kohsuke/github/Requester.java | 11 ++-- .../org/kohsuke/github/RepositoryTest.java | 25 +++++++++ 4 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/kohsuke/github/GHPermission.java diff --git a/src/main/java/org/kohsuke/github/GHPermission.java b/src/main/java/org/kohsuke/github/GHPermission.java new file mode 100644 index 0000000000..8bc28c719a --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHPermission.java @@ -0,0 +1,53 @@ +/* + * The MIT License + * + * Copyright 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.kohsuke.github; + +/** + * Permission for a user in a repository. + * @see API + */ +public class GHPermission { + + private String permission; + private GHUser user; + + /** + * @return one of {@code admin}, {@code write}, {@code read}, or {@code none} + */ + public String getPermission() { + return permission; + } + + public GHUser getUser() { + return user; + } + + void wrapUp(GitHub root) { + if (user != null) { + user.root = root; + } + } + +} diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index f34ea85004..d3af91a210 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -480,6 +480,18 @@ public Set 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 + */ + public GHPermission getPermission(String user) throws IOException { + GHPermission perm = root.retrieve().withHeader("Accept", "application/vnd.github.korra-preview").to(getApiTailUrl("collaborators/" + user + "/permission"), GHPermission.class); + perm.wrapUp(root); + return perm; + } + /** * If this repository belongs to an organization, return a set of teams. */ diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 70c48940c0..2eb3f282fb 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -598,11 +598,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/RepositoryTest.java b/src/test/java/org/kohsuke/github/RepositoryTest.java index dae2892a45..21625fbf89 100644 --- a/src/test/java/org/kohsuke/github/RepositoryTest.java +++ b/src/test/java/org/kohsuke/github/RepositoryTest.java @@ -1,9 +1,11 @@ package org.kohsuke.github; +import java.io.FileNotFoundException; import org.junit.Test; import org.kohsuke.github.GHRepository.Contributor; import java.io.IOException; +import org.junit.Ignore; /** * @author Kohsuke Kawaguchi @@ -40,6 +42,29 @@ public void listContributors() throws IOException { assertTrue(kohsuke); } + @Ignore("depends on who runs this test whether it can pass or not") + @Test + public void getPermission() throws Exception { + GHRepository r = gitHub.getOrganization("cloudbeers").getRepository("yolo"); + assertEquals("admin", r.getPermission("jglick").getPermission()); + assertEquals("read", r.getPermission("dude").getPermission()); + r = gitHub.getOrganization("cloudbees").getRepository("private-repo-not-writable-by-me"); + try { + r.getPermission("jglick"); + fail(); + } catch (FileNotFoundException x) { + x.printStackTrace(); // good + } + r = gitHub.getOrganization("apache").getRepository("groovy"); + try { + r.getPermission("jglick"); + fail(); + } catch (HttpException x) { + x.printStackTrace(); // good + assertEquals(403, x.getResponseCode()); + } + } + private GHRepository getRepository() throws IOException { return gitHub.getOrganization("github-api-test-org").getRepository("jenkins"); } From a746a310bcac5ffbd9670010b24eb83b9257fb07 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Sat, 17 Dec 2016 07:50:55 -0800 Subject: [PATCH 02/12] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index bd00bca40c..449ac7877f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.82 + 1.83-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.82 + HEAD From 470da06ecf3d4f36995c440103eb8c17ac799f3b Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Sat, 17 Dec 2016 08:17:20 -0800 Subject: [PATCH 03/12] Cleaning up javadoc warnings --- pom.xml | 2 +- src/main/java/org/kohsuke/github/GHRelease.java | 4 +--- src/main/java/org/kohsuke/github/GHReleaseBuilder.java | 1 - src/main/java/org/kohsuke/github/GHRepository.java | 3 +-- src/main/java/org/kohsuke/github/GHTree.java | 1 - src/main/java/org/kohsuke/github/GitHub.java | 1 - 6 files changed, 3 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 449ac7877f..40bf81cbf2 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.kohsuke pom - 14 + 17 github-api diff --git a/src/main/java/org/kohsuke/github/GHRelease.java b/src/main/java/org/kohsuke/github/GHRelease.java index b61dc62b38..96421da415 100644 --- a/src/main/java/org/kohsuke/github/GHRelease.java +++ b/src/main/java/org/kohsuke/github/GHRelease.java @@ -121,9 +121,7 @@ static GHRelease[] wrap(GHRelease[] releases, GHRepository owner) { * Java 7 or greater. Options for fixing this for earlier JVMs can be found here * http://stackoverflow.com/questions/12361090/server-name-indication-sni-on-java but involve more complicated * handling of the HTTP requests to github's API. - * - * @throws IOException - */ + */ public GHAsset uploadAsset(File file, String contentType) throws IOException { Requester builder = new Requester(owner.root); diff --git a/src/main/java/org/kohsuke/github/GHReleaseBuilder.java b/src/main/java/org/kohsuke/github/GHReleaseBuilder.java index 04e368f2b2..b1daac956e 100644 --- a/src/main/java/org/kohsuke/github/GHReleaseBuilder.java +++ b/src/main/java/org/kohsuke/github/GHReleaseBuilder.java @@ -33,7 +33,6 @@ public GHReleaseBuilder body(String body) { * * @param commitish Defaults to the repository’s default branch (usually "master"). Unused if the Git tag * already exists. - * @return */ public GHReleaseBuilder commitish(String commitish) { if (commitish != null) { diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 4a0d410621..73bcd99d19 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -449,7 +449,6 @@ public GHPersonSet getCollaborators() throws IOException { * Lists up the collaborators on this repository. * * @return Users - * @throws IOException */ public PagedIterable listCollaborators() throws IOException { return listUsers("collaborators"); @@ -818,7 +817,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..5d613fe985 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -416,7 +416,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 From 26c20a7a224a066bb06b71461143ec517669dd61 Mon Sep 17 00:00:00 2001 From: "Jeffrey.Nelson" Date: Thu, 22 Dec 2016 11:55:01 -0600 Subject: [PATCH 04/12] add branch protection attributes --- .../java/org/kohsuke/github/GHBranch.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GHBranch.java b/src/main/java/org/kohsuke/github/GHBranch.java index 4c80db9ef4..9870732d0d 100644 --- a/src/main/java/org/kohsuke/github/GHBranch.java +++ b/src/main/java/org/kohsuke/github/GHBranch.java @@ -1,13 +1,16 @@ package org.kohsuke.github; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.kohsuke.github.BranchProtection.RequiredStatusChecks; +import static org.kohsuke.github.Previews.LOKI; import java.io.IOException; import java.util.Arrays; import java.util.Collection; -import static org.kohsuke.github.Previews.LOKI; +import org.kohsuke.github.BranchProtection.RequiredStatusChecks; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * A branch in a repository. @@ -22,6 +25,10 @@ public class GHBranch { private String name; private Commit commit; + @JsonProperty("protected") + private boolean protection; + private String protection_url; + public static class Commit { String sha; @@ -44,6 +51,15 @@ public GHRepository getOwner() { public String getName() { return name; } + + public boolean isProtected() { + return protection; + } + + public String getProtection_url() { + return protection_url; + } + /** * The commit that this branch currently points to. From 9d03435aa14703c1a573371919e4151ad84a0848 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 5 Jan 2017 09:21:32 +0000 Subject: [PATCH 05/12] Expose Rate Limit Headers Exposes the rate limit header responses so that consumers of the API can proactively tune their usage --- src/main/java/org/kohsuke/github/GitHub.java | 27 +++++++ .../java/org/kohsuke/github/Requester.java | 76 +++++++++++++++++-- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 5d613fe985..fc258e8a02 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -50,6 +50,8 @@ import java.util.Set; import java.util.TimeZone; +import java.util.logging.Level; +import javax.annotation.CheckForNull; import org.apache.commons.codec.Charsets; import org.apache.commons.codec.binary.Base64; @@ -90,6 +92,9 @@ public class GitHub { private HttpConnector connector = HttpConnector.DEFAULT; + private final Object headerRateLimitLock = new Object(); + private GHRateLimit headerRateLimit = null; + /** * Creates a client API root object. * @@ -300,6 +305,28 @@ public GHRateLimit getRateLimit() throws IOException { } } + /*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 recentlt observed rate limit data or {@code null}. + */ + @CheckForNull + public GHRateLimit lastRateLimit() { + return headerRateLimit; + } + /** * Gets the {@link GHUser} that represents yourself. */ diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 70c48940c0..21b3afd150 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) { From dfea424b9412f4162ea8b94114c15f92897f5baf Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 5 Jan 2017 09:36:58 +0000 Subject: [PATCH 06/12] Expose the API url used by the GitHub --- src/main/java/org/kohsuke/github/GitHub.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index fc258e8a02..4d9d04c089 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -259,6 +259,10 @@ public HttpConnector getConnector() { return connector; } + public String getApiUrl() { + return apiUrl; + } + /** * Sets the custom connector used to make requests to GitHub. */ From 6fcddf4a475e5b59b8e7644d44e857126635a056 Mon Sep 17 00:00:00 2001 From: Stephen Connolly Date: Thu, 5 Jan 2017 10:24:16 +0000 Subject: [PATCH 07/12] Some usage patterns require more pro-active rate limit queries --- src/main/java/org/kohsuke/github/GitHub.java | 61 +++++++++++++------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 4d9d04c089..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,19 +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. @@ -94,6 +92,7 @@ public class GitHub { private final Object headerRateLimitLock = new Object(); private GHRateLimit headerRateLimit = null; + private volatile GHRateLimit rateLimit = null; /** * Creates a client API root object. @@ -296,16 +295,16 @@ 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; } } @@ -324,11 +323,33 @@ public GHRateLimit getRateLimit() throws IOException { * 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 recentlt observed rate limit data or {@code null}. + * @return the most recently observed rate limit data or {@code null}. */ @CheckForNull public GHRateLimit lastRateLimit() { - return headerRateLimit; + 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; } /** From 1212ae3eb3f854ea903675ffe6535af6982d5808 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 9 Jan 2017 16:06:05 -0800 Subject: [PATCH 08/12] Touch up for uniformity - Prefer typed 'URL' over 'String' that is URL - Mark API as @Preview to communicate that this is subject to change More branch protection stuff needs to be added. See https://developer.github.com/v3/repos/branches/ --- src/main/java/org/kohsuke/github/GHBranch.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kohsuke/github/GHBranch.java b/src/main/java/org/kohsuke/github/GHBranch.java index 9870732d0d..f54bea9f47 100644 --- a/src/main/java/org/kohsuke/github/GHBranch.java +++ b/src/main/java/org/kohsuke/github/GHBranch.java @@ -3,6 +3,7 @@ import static org.kohsuke.github.Previews.LOKI; import java.io.IOException; +import java.net.URL; import java.util.Arrays; import java.util.Collection; @@ -51,13 +52,21 @@ public GHRepository getOwner() { public String getName() { return name; } - + + /** + * Returns true if the push to this branch is restricted via branch protection. + */ + @Preview @Deprecated public boolean isProtected() { - return protection; + return protection; } - public String getProtection_url() { - return protection_url; + /** + * Returns API URL that deals with the protection of this branch. + */ + @Preview @Deprecated + public URL getProtectionUrl() { + return GitHub.parseURL(protection_url); } From 6bfeb54f3c5116e241ad8ddd130667cd398bf459 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 9 Jan 2017 16:14:30 -0800 Subject: [PATCH 09/12] Just exposing permission type enum until there's more to this relation than one property --- src/main/java/org/kohsuke/github/GHPermission.java | 8 +++++++- .../java/org/kohsuke/github/GHPermissionType.java | 11 +++++++++++ src/main/java/org/kohsuke/github/GHRepository.java | 13 +++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/kohsuke/github/GHPermissionType.java diff --git a/src/main/java/org/kohsuke/github/GHPermission.java b/src/main/java/org/kohsuke/github/GHPermission.java index 8bc28c719a..51a808dfc0 100644 --- a/src/main/java/org/kohsuke/github/GHPermission.java +++ b/src/main/java/org/kohsuke/github/GHPermission.java @@ -24,11 +24,13 @@ package org.kohsuke.github; +import java.util.Locale; + /** * Permission for a user in a repository. * @see API */ -public class GHPermission { +/*package*/ class GHPermission { private String permission; private GHUser user; @@ -40,6 +42,10 @@ public String getPermission() { return permission; } + public GHPermissionType getPermissionType() { + return Enum.valueOf(GHPermissionType.class, permission.toUpperCase(Locale.ENGLISH)); + } + public GHUser getUser() { return user; } diff --git a/src/main/java/org/kohsuke/github/GHPermissionType.java b/src/main/java/org/kohsuke/github/GHPermissionType.java new file mode 100644 index 0000000000..d3e2bd0909 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHPermissionType.java @@ -0,0 +1,11 @@ +package org.kohsuke.github; + +/** + * @author Kohsuke Kawaguchi + */ +public enum GHPermissionType { + ADMIN, + WRITE, + READ, + NONE +} diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index d3af91a210..cca1d7a8e6 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -486,10 +486,19 @@ public Set getCollaboratorNames() throws IOException { * @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 */ - public GHPermission getPermission(String user) throws IOException { + public GHPermissionType getPermission(String user) throws IOException { GHPermission perm = root.retrieve().withHeader("Accept", "application/vnd.github.korra-preview").to(getApiTailUrl("collaborators/" + user + "/permission"), GHPermission.class); perm.wrapUp(root); - return perm; + 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 + */ + public GHPermissionType getPermission(GHUser u) throws IOException { + return getPermission(u.getLogin()); } /** From d1c900a6206ddbeb78616a92841b0667a08a8a11 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 9 Jan 2017 16:18:40 -0800 Subject: [PATCH 10/12] Marking the fact that these APIs are still in preview and subject to change --- src/main/java/org/kohsuke/github/GHRepository.java | 4 +++- src/main/java/org/kohsuke/github/Previews.java | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index cca1d7a8e6..3235fea3c4 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -486,8 +486,9 @@ public Set getCollaboratorNames() throws IOException { * @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().withHeader("Accept", "application/vnd.github.korra-preview").to(getApiTailUrl("collaborators/" + user + "/permission"), GHPermission.class); + GHPermission perm = root.retrieve().withPreview(KORRA).to(getApiTailUrl("collaborators/" + user + "/permission"), GHPermission.class); perm.wrapUp(root); return perm.getPermissionType(); } @@ -497,6 +498,7 @@ public GHPermissionType getPermission(String user) throws IOException { * @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()); } 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"; } From 911e8d21a717842221f42cd49de0dd543250a807 Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 9 Jan 2017 16:26:58 -0800 Subject: [PATCH 11/12] Made the test case runnable, at least for me --- .../github/AbstractGitHubApiTestBase.java | 15 ++++++++++ src/test/java/org/kohsuke/github/AppTest.java | 13 --------- .../org/kohsuke/github/RepositoryTest.java | 29 ++++++++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) 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 21625fbf89..9955c34956 100644 --- a/src/test/java/org/kohsuke/github/RepositoryTest.java +++ b/src/test/java/org/kohsuke/github/RepositoryTest.java @@ -1,11 +1,10 @@ package org.kohsuke.github; -import java.io.FileNotFoundException; import org.junit.Test; import org.kohsuke.github.GHRepository.Contributor; +import java.io.FileNotFoundException; import java.io.IOException; -import org.junit.Ignore; /** * @author Kohsuke Kawaguchi @@ -42,19 +41,12 @@ public void listContributors() throws IOException { assertTrue(kohsuke); } - @Ignore("depends on who runs this test whether it can pass or not") @Test public void getPermission() throws Exception { - GHRepository r = gitHub.getOrganization("cloudbeers").getRepository("yolo"); - assertEquals("admin", r.getPermission("jglick").getPermission()); - assertEquals("read", r.getPermission("dude").getPermission()); - r = gitHub.getOrganization("cloudbees").getRepository("private-repo-not-writable-by-me"); - try { - r.getPermission("jglick"); - fail(); - } catch (FileNotFoundException x) { - x.printStackTrace(); // good - } + 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"); @@ -63,6 +55,17 @@ public void getPermission() throws Exception { 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 { From 13184e72e10b9b5f56d3e795f2817940fb994dab Mon Sep 17 00:00:00 2001 From: Kohsuke Kawaguchi Date: Mon, 9 Jan 2017 16:37:56 -0800 Subject: [PATCH 12/12] [maven-release-plugin] prepare release github-api-1.83 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 40bf81cbf2..714567f3e6 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.83-SNAPSHOT + 1.83 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/ - HEAD + github-api-1.83