Compare commits

...

86 Commits

Author SHA1 Message Date
Kohsuke Kawaguchi
b0df93bbcb [maven-release-plugin] prepare release github-api-1.78 2016-10-24 14:10:30 -07:00
Kohsuke Kawaguchi
290d0b226a Merge pull request #295 from jglick/pageSize
Use maximum permitted page size
2016-10-24 14:04:05 -07:00
Kohsuke Kawaguchi
8b3469610c Merge pull request #300 from stephenc/commit-dates
Expose the commit dates
2016-10-24 14:03:14 -07:00
Stephen Connolly
50b47fb73b Expose the commit dates 2016-10-24 21:12:56 +01:00
Jesse Glick
38983df42d If we are a returning a collection of all things, we might as well use the maximum page size to minimize HTTP requests. 2016-09-19 09:48:23 -07:00
Kohsuke Kawaguchi
df963cb71c [maven-release-plugin] prepare for next development iteration 2016-08-05 21:32:15 -07:00
Kohsuke Kawaguchi
e9368fb04e [maven-release-plugin] prepare release github-api-1.77 2016-08-05 21:32:10 -07:00
Kohsuke Kawaguchi
e2a1630cf4 findbug taming 2016-08-05 21:28:54 -07:00
Kohsuke Kawaguchi
4f15b7c9fa NPE fix. type can be null 2016-08-05 21:19:32 -07:00
Kohsuke Kawaguchi
63f500ad7f Fixed issue #286
List commit API (https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository) already populates short info, and so populate() call could be excessive.

 It's possible that the short info is always available and therefore there's never a need to call populate(), but that assumption is hard to test, so I'm leaving that in
2016-08-05 21:11:00 -07:00
Kohsuke Kawaguchi
a9fb4546e1 Constants for preview media types 2016-08-05 20:56:11 -07:00
Kohsuke Kawaguchi
cabbbf7f02 Handle 404 that represents "no license" 2016-08-05 20:50:39 -07:00
Kohsuke Kawaguchi
59324b0082 Add preview media type header explicitly 2016-08-05 20:47:08 -07:00
Kohsuke Kawaguchi
6a356c82a5 Fixed up tests 2016-08-05 20:44:45 -07:00
Kohsuke Kawaguchi
70f0f5714a While a use of custom HttpConnector is clever, it doesn't fit the current idiom of this library. 2016-08-05 20:44:10 -07:00
Kohsuke Kawaguchi
07b527a0f2 Enumeration in GitHub should be PagedIterable 2016-08-05 20:40:34 -07:00
Kohsuke Kawaguchi
80aa75aab1 Added the wrap() method for a backpointer 2016-08-05 20:40:23 -07:00
Kohsuke Kawaguchi
0cf4211aa5 Merged GHLicense & GHLicenseBase
Elsewhere in this library, whenever there are multiple forms of the same
object, we map that to the same class and use lazy data retrieval to
fill missing fields.
2016-08-05 20:36:46 -07:00
Kohsuke Kawaguchi
1de02a5099 Added a marker for preview APIs 2016-08-05 20:19:36 -07:00
Duncan Dickinson
bb1cecb95b PR-284: license API support
Had to do git-diff | git-apply to avoid whitespe changes to GHRepository
2016-08-05 20:11:33 -07:00
Kohsuke Kawaguchi
d82397a173 doc fix 2016-08-05 20:00:05 -07:00
Kohsuke Kawaguchi
856cf5e568 Better type safety by splitting RateLimitHandler and AbuseLimitHandler
While the signature is the same, headers that they expect are different,
so any non-trivial logic cannot be reused.
2016-08-05 19:58:04 -07:00
Matt Mitchell
9f5a6ee549 Implement an abuse handler
If too many requests are made within X amount of time (not the traditional hourly rate limit), github may begin returning 403.  Then we should wait for a bit to attempt to access the API again.  In this case, we parse out the Retry-After field returned and sleep until that (it's usually 60 seconds)
2016-07-22 13:16:12 -07:00
Kohsuke Kawaguchi
a2f0837d14 Issue #279: added another overload that takes permission 2016-06-03 20:56:17 -07:00
Kohsuke Kawaguchi
c7f6889534 Issue #258: updated OkHttp that handles Cache control headers better 2016-06-03 20:31:12 -07:00
Kohsuke Kawaguchi
0415326d09 Issue #261: handle 204 no content correctly 2016-06-03 20:27:29 -07:00
Kohsuke Kawaguchi
16a0f8ece0 [maven-release-plugin] prepare for next development iteration 2016-06-03 00:19:15 -07:00
Kohsuke Kawaguchi
5d1ef296b3 [maven-release-plugin] prepare release github-api-1.76 2016-06-03 00:19:09 -07:00
Kohsuke Kawaguchi
16dbcde90b Shut up FindBugs! 2016-06-03 00:16:20 -07:00
Kohsuke Kawaguchi
50cbf25c72 Shut up FindBugs 2016-06-03 00:09:25 -07:00
Kohsuke Kawaguchi
7b87de2b4c Giving it a bit of delay in the hope that it removes flakiness of tests 2016-06-03 00:04:36 -07:00
Kohsuke Kawaguchi
1ce54a7925 Bug fix in toString() 2016-06-02 23:55:55 -07:00
Kohsuke Kawaguchi
1bfe7dd99b Issue #264: wait for the repo to finish forking 2016-06-02 23:50:18 -07:00
Kohsuke Kawaguchi
27e855ddbd Issue 262: added support for branch protection API 2016-06-02 23:40:37 -07:00
Kohsuke Kawaguchi
3d1bed0f8f In JDK I'm using (Java8), I get a delegating HttpURLConnection that breaks the hack to set the method.
This change makes this hack even worse, but this is the only way I can think of, since I cannot update HttpURLConnection.methods that is static final.
2016-06-02 23:40:14 -07:00
Kohsuke Kawaguchi
5c9ea9b63a Added a fluent version 2016-06-02 23:27:52 -07:00
Kohsuke Kawaguchi
7c034f5670 Doc improvement 2016-06-02 22:43:58 -07:00
Kohsuke Kawaguchi
3b9f5a417a Formatting changes 2016-06-02 22:43:06 -07:00
Kohsuke Kawaguchi
cde501af8d More meaningful toString() method
Produce toString without dilligently adding it to every single class.
Rely on heuristics to cut down the number of fields to show.
2016-06-02 22:38:38 -07:00
Kohsuke Kawaguchi
3c5592c1c8 Following the convention with GHMyself.getEmails2()
This way the method is more discoverable with IDE auto-completion
2016-06-02 22:05:24 -07:00
Kohsuke Kawaguchi
2508e022bb Merge pull request #272 from noctarius/master
Added support for the extended stargazers API in Github V3 API
2016-06-03 14:04:24 +09:00
Kohsuke Kawaguchi
01fcbc24e8 Merge pull request #282 from apemberton/org-fix
related to JENKINS-34834. updating test for similar condition
2016-06-03 13:52:26 +09:00
Kohsuke Kawaguchi
204e639679 Merge pull request #281 from apemberton/master
Add Slug to GHTeam per v3 API: https://developer.github.com/v3/orgs/t…
2016-06-03 13:51:47 +09:00
Kohsuke Kawaguchi
3d301ec730 Merge pull request #278 from jglick/javadoc
Fixed broken link
2016-06-03 13:50:37 +09:00
Kohsuke Kawaguchi
9ab6d57019 Merge pull request #277 from rhels/patch-1
Updated Date was wrong
2016-06-03 13:50:22 +09:00
Kohsuke Kawaguchi
37c473130f Merge pull request #276 from thug-gamer/patch-1
Add support to delete a team
2016-06-03 13:50:11 +09:00
Andy Pemberton
5f95987a48 related to JENKINS-34834. updating test for similar condition 2016-05-14 20:07:04 -04:00
Andy Pemberton
d530b34073 Add Slug to GHTeam per v3 API: https://developer.github.com/v3/orgs/teams/ 2016-05-14 08:58:42 -04:00
Jesse Glick
35dec7a5ec Fixed broken link. 2016-05-03 18:19:57 -04:00
Konda Reddy
baedad8124 Updated Date was wrong 2016-04-30 13:20:44 -05:00
thug-gamer
007378c3a6 Add support to delete a team
Add a method to delete a team.
2016-04-29 11:41:14 +02:00
noctarius
beae9fd6ec Added support for the extended stargazers API in Github V3 API 2016-04-14 07:22:17 +02:00
Kohsuke Kawaguchi
b7507076c6 Merge pull request #270 from szpak/issue/269-reopenMilestone
[#269] Add reopen method on GHMilestone
2016-04-13 16:15:39 -07:00
Kohsuke Kawaguchi
6b5ade3ca0 [maven-release-plugin] prepare for next development iteration 2016-04-13 13:08:02 -07:00
Kohsuke Kawaguchi
715192d26c [maven-release-plugin] prepare release github-api-1.75 2016-04-13 13:07:58 -07:00
Marcin Zajaczkowski
ce140460af [#269] Add reopen method on GHMilestone 2016-04-04 17:54:55 +02:00
Kohsuke Kawaguchi
d30b0403ce [maven-release-plugin] prepare for next development iteration 2016-03-18 19:22:37 -07:00
Kohsuke Kawaguchi
255c993548 [maven-release-plugin] prepare release github-api-1.74 2016-03-18 19:22:34 -07:00
Kohsuke Kawaguchi
557ae4165c Not important 2016-03-18 19:19:51 -07:00
Kohsuke Kawaguchi
a31395ed80 Signature fix for Java5 2016-03-18 19:15:52 -07:00
Kohsuke Kawaguchi
397886d289 Excluding a flaky test 2016-03-18 19:08:51 -07:00
Kohsuke Kawaguchi
7307bec2ae Merge pull request #259 from Shredder121/animal-sniffer
Animal sniffer
2016-03-18 18:27:22 -07:00
Ruben Dijkstra
0cd5147e1a Java 5 doesn't have TimeUnit.HOURS
http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/TimeUnit.html
2016-03-12 22:01:25 +01:00
Ruben Dijkstra
36d5b092d7 Java 5 doesn't have new String(byte[], Charset)
http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/String.html
2016-03-12 21:59:51 +01:00
Ruben Dijkstra
f9014dbab3 Convert to legacy Throwable.initCause() 2016-03-12 21:58:27 +01:00
Ruben Dijkstra
755d5f77ea Java 5 doesn't have Arrays.copyOf()
http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Arrays.html
2016-03-12 21:56:56 +01:00
Ruben Dijkstra
906d9af7b7 Include the animal sniffer plugin
7a78f9f5aa set the compiler level back to 5, but there are no guarantees that we don't accidentally use any features from newer class libraries.
2016-03-12 21:48:50 +01:00
Kohsuke Kawaguchi
14dcb37ee1 Not all caller wants GET 2016-03-11 23:29:58 -08:00
Kohsuke Kawaguchi
c1c2a27358 Doc improvement 2016-03-11 23:21:07 -08:00
Kohsuke Kawaguchi
5ab9657f9c Merge branch 'master' of github.com:kohsuke/github-api 2016-03-11 23:16:37 -08:00
Kohsuke Kawaguchi
7a78f9f5aa No need to require 1.6 2016-03-11 23:16:24 -08:00
Kohsuke Kawaguchi
ac8c65f062 Merge pull request #251
Conflicts:
	src/main/java/org/kohsuke/github/GitHub.java
2016-03-11 23:14:01 -08:00
Kohsuke Kawaguchi
1954a9f3f8 This isn't just about API URL but it also checks the valid credential 2016-03-11 23:12:50 -08:00
Kohsuke Kawaguchi
3b764f9c90 Don't lose the original problem 2016-03-11 23:11:22 -08:00
Kohsuke Kawaguchi
10f55cc549 Checking another header
I think it's better to pick a header that's unique to GitHub.
2016-03-11 23:10:41 -08:00
Kohsuke Kawaguchi
cd8d955646 Documenting what one gets 2016-03-11 23:06:21 -08:00
Kohsuke Kawaguchi
bba07c9080 Merge pull request #253 from cyrille-leclerc/fix-infinite-loop
Fix #252: infinite loop because the "hypertext engine" generates invalid URLs
2016-03-11 22:59:00 -08:00
Kohsuke Kawaguchi
dba84a33b9 Logger should be static 2016-03-11 22:55:58 -08:00
Kohsuke Kawaguchi
ae49166aa2 No such parameter exists on this method 2016-03-11 22:55:44 -08:00
Kohsuke Kawaguchi
e09185fd0e Logger should be static 2016-03-11 22:51:48 -08:00
Cyrille Le Clerc
56379bb3b9 Fix broken log message in GitHub.java and cleanup code as recommended by @jglick 2016-03-07 19:25:42 +01:00
Cyrille Le Clerc
027e4b4f25 Better error message: introduce HttpException, subclass of IOException with url, http responseCode and http responseMessage to help exception handling. 2016-03-06 18:34:27 +01:00
Cyrille Le Clerc
ba951cb6e3 Fix #252: infinite loop because the "hypertext engine" may duplicate '?' generating invalid "https://api.github.com/notifications?all=true&page=2?all=true" instead of "https://api.github.com/notifications?all=true&page=2&all=true". A better fix will be to prevent duplication of parameters ("all=true" in this case). 2016-03-06 18:27:05 +01:00
Manuel Recena
ae85cf4b6c Improve checkApiUrlValidity() method to support the private mode in GitHub Enterprise servers 2016-03-05 17:42:25 +01:00
Kohsuke Kawaguchi
dbc79f8c42 Fixing issue raised in https://github.com/kohsuke/github-api/pull/247
From Shredder121,
--------------------
Only the HttpURLConnection.method was set by that change. Not the
Requester.method.

This means that Requester.method is still set to POST, isMethodWithBody
will return true, and uc.setDoOutput(true) will be called.

I use Okhttp, and their HttpURLConnectionImpl's method changes to POST
if you tell it to open a stream to write to.
2016-03-01 19:46:50 -08:00
Kohsuke Kawaguchi
54c3070607 [maven-release-plugin] prepare for next development iteration 2016-02-29 21:03:34 -08:00
36 changed files with 1257 additions and 84 deletions

27
pom.xml
View File

@@ -7,7 +7,7 @@
</parent> </parent>
<artifactId>github-api</artifactId> <artifactId>github-api</artifactId>
<version>1.73</version> <version>1.78</version>
<name>GitHub API for Java</name> <name>GitHub API for Java</name>
<url>http://github-api.kohsuke.org/</url> <url>http://github-api.kohsuke.org/</url>
<description>GitHub API for Java</description> <description>GitHub API for Java</description>
@@ -16,7 +16,7 @@
<connection>scm:git:git@github.com/kohsuke/${project.artifactId}.git</connection> <connection>scm:git:git@github.com/kohsuke/${project.artifactId}.git</connection>
<developerConnection>scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git</developerConnection> <developerConnection>scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git</developerConnection>
<url>http://${project.artifactId}.kohsuke.org/</url> <url>http://${project.artifactId}.kohsuke.org/</url>
<tag>github-api-1.73</tag> <tag>github-api-1.78</tag>
</scm> </scm>
<distributionManagement> <distributionManagement>
@@ -34,6 +34,27 @@
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<version>1.15</version>
<configuration>
<signature>
<groupId>org.codehaus.mojo.signature</groupId>
<artifactId>java15</artifactId>
<version>1.0</version>
</signature>
</configuration>
<executions>
<execution>
<id>ensure-java-1.5-class-library</id>
<phase>test</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<groupId>com.infradna.tool</groupId> <groupId>com.infradna.tool</groupId>
<artifactId>bridge-method-injector</artifactId> <artifactId>bridge-method-injector</artifactId>
@@ -114,7 +135,7 @@
<dependency> <dependency>
<groupId>com.squareup.okhttp</groupId> <groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp-urlconnection</artifactId> <artifactId>okhttp-urlconnection</artifactId>
<version>2.0.0</version> <version>2.7.5</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>

View File

@@ -0,0 +1,63 @@
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 abuse limit is hit.
*
* @author Kohsuke Kawaguchi
* @see GitHubBuilder#withAbuseLimitHandler(AbuseLimitHandler)
* @see <a href="https://developer.github.com/v3/#abuse-rate-limits">documentation</a>
* @see RateLimitHandler
*/
public abstract class AbuseLimitHandler {
/**
* Called when the library encounters HTTP error indicating that the API abuse limit is reached.
*
* <p>
* 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 <a href="https://developer.github.com/v3/#abuse-rate-limits">API documentation from GitHub</a>
* @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;
/**
* Wait until the API abuse "wait time" is passed.
*/
public static final AbuseLimitHandler WAIT = new AbuseLimitHandler() {
@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("Retry-After");
if (v==null) return 60 * 1000; // can't tell, return 1 min
return Math.max(1000, Long.parseLong(v)*1000);
}
};
/**
* Fail immediately.
*/
public static final AbuseLimitHandler FAIL = new AbuseLimitHandler() {
@Override
public void onError(IOException e, HttpURLConnection uc) throws IOException {
throw (IOException)new IOException("Abust limit reached").initCause(e);
}
};
}

View File

@@ -0,0 +1,21 @@
package org.kohsuke.github;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.List;
/**
* @author Kohsuke Kawaguchi
* @see GHBranch#disableProtection()
*/
@SuppressFBWarnings(value = {"UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD"}, justification = "JSON API")
class BranchProtection {
boolean enabled;
RequiredStatusChecks requiredStatusChecks;
static class RequiredStatusChecks {
EnforcementLevel enforcement_level;
List<String> contexts = new ArrayList<String>();
}
}

View File

@@ -0,0 +1,14 @@
package org.kohsuke.github;
import java.util.Locale;
/**
* @author Kohsuke Kawaguchi
*/
public enum EnforcementLevel {
OFF, NON_ADMINS, EVERYONE;
public String toString() {
return name().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -1,6 +1,13 @@
package org.kohsuke.github; package org.kohsuke.github;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.kohsuke.github.BranchProtection.RequiredStatusChecks;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import static org.kohsuke.github.Previews.LOKI;
/** /**
* A branch in a repository. * A branch in a repository.
@@ -8,7 +15,7 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
* @author Yusuke Kokubo * @author Yusuke Kokubo
*/ */
@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", @SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD",
"NP_UNWRITTEN_FIELD"}, justification = "JSON API") "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD"}, justification = "JSON API")
public class GHBranch { public class GHBranch {
private GitHub root; private GitHub root;
private GHRepository owner; private GHRepository owner;
@@ -44,6 +51,44 @@ public class GHBranch {
public String getSHA1() { public String getSHA1() {
return commit.sha; return commit.sha;
} }
/**
* Disables branch protection and allows anyone with push access to push changes.
*/
@Preview @Deprecated
public void disableProtection() throws IOException {
BranchProtection bp = new BranchProtection();
bp.enabled = false;
setProtection(bp);
}
/**
* Enables branch protection to control what commit statuses are required to push.
*
* @see GHCommitStatus#getContext()
*/
@Preview @Deprecated
public void enableProtection(EnforcementLevel level, Collection<String> contexts) throws IOException {
BranchProtection bp = new BranchProtection();
bp.enabled = true;
bp.requiredStatusChecks = new RequiredStatusChecks();
bp.requiredStatusChecks.enforcement_level = level;
bp.requiredStatusChecks.contexts.addAll(contexts);
setProtection(bp);
}
@Preview @Deprecated
public void enableProtection(EnforcementLevel level, String... contexts) throws IOException {
enableProtection(level, Arrays.asList(contexts));
}
private void setProtection(BranchProtection bp) throws IOException {
new Requester(root).method("PATCH").withPreview(LOKI)._with("protection",bp).to(getApiRoute());
}
String getApiRoute() {
return owner.getApiTailUrl("/branches/"+name);
}
@Override @Override
public String toString() { public String toString() {

View File

@@ -2,12 +2,12 @@ package org.kohsuke.github;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.AbstractList; import java.util.AbstractList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.List; import java.util.List;
/** /**
@@ -42,11 +42,19 @@ public class GHCommit {
return author; return author;
} }
public Date getAuthoredDate() {
return GitHub.parseDate(author.date);
}
@WithBridgeMethods(value = GHAuthor.class, castRequired = true) @WithBridgeMethods(value = GHAuthor.class, castRequired = true)
public GitUser getCommitter() { public GitUser getCommitter() {
return committer; return committer;
} }
public Date getCommitDate() {
return GitHub.parseDate(author.date);
}
/** /**
* Commit message. * Commit message.
*/ */
@@ -63,6 +71,7 @@ public class GHCommit {
* @deprecated Use {@link GitUser} instead. * @deprecated Use {@link GitUser} instead.
*/ */
public static class GHAuthor extends GitUser { public static class GHAuthor extends GitUser {
private String date;
} }
public static class Stats { public static class Stats {
@@ -179,7 +188,8 @@ public class GHCommit {
public ShortInfo getCommitShortInfo() throws IOException { public ShortInfo getCommitShortInfo() throws IOException {
populate(); if (commit==null)
populate();
return commit; return commit;
} }
@@ -271,10 +281,29 @@ public class GHCommit {
return resolveUser(author); return resolveUser(author);
} }
/**
* Gets the date the change was authored on.
* @return the date the change was authored on.
* @throws IOException if the information was not already fetched and an attempt at fetching the information failed.
*/
public Date getAuthoredDate() throws IOException {
return getCommitShortInfo().getAuthoredDate();
}
public GHUser getCommitter() throws IOException { public GHUser getCommitter() throws IOException {
return resolveUser(committer); return resolveUser(committer);
} }
/**
* Gets the date the change was committed on.
*
* @return the date the change was committed on.
* @throws IOException if the information was not already fetched and an attempt at fetching the information failed.
*/
public Date getCommitDate() throws IOException {
return getCommitShortInfo().getCommitDate();
}
private GHUser resolveUser(User author) throws IOException { private GHUser resolveUser(User author) throws IOException {
if (author==null || author.login==null) return null; if (author==null || author.login==null) return null;
return owner.root.getUser(author.login); return owner.root.getUser(author.login);

View File

@@ -1,8 +1,6 @@
package org.kohsuke.github; package org.kohsuke.github;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Date;
/** /**
* Represents a status of a commit. * Represents a status of a commit.
@@ -10,6 +8,7 @@ import java.util.Date;
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
* @see GHRepository#getLastCommitStatus(String) * @see GHRepository#getLastCommitStatus(String)
* @see GHCommit#getLastStatus() * @see GHCommit#getLastStatus()
* @see GHRepository#createCommitStatus(String, GHCommitState, String, String)
*/ */
public class GHCommitStatus extends GHObject { public class GHCommitStatus extends GHObject {
String state; String state;

View File

@@ -4,8 +4,6 @@ import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
import java.util.Date;
/** /**
* The model user for comparing 2 commits in the GitHub API. * The model user for comparing 2 commits in the GitHub API.
@@ -72,7 +70,9 @@ public class GHCompare {
* @return A copy of the array being stored in the class. * @return A copy of the array being stored in the class.
*/ */
public Commit[] getCommits() { public Commit[] getCommits() {
return Arrays.copyOf(commits, commits.length); Commit[] newValue = new Commit[commits.length];
System.arraycopy(commits, 0, newValue, 0, commits.length);
return newValue;
} }
/** /**
@@ -80,7 +80,9 @@ public class GHCompare {
* @return A copy of the array being stored in the class. * @return A copy of the array being stored in the class.
*/ */
public GHCommit.File[] getFiles() { public GHCommit.File[] getFiles() {
return Arrays.copyOf(files, files.length); GHCommit.File[] newValue = new GHCommit.File[files.length];
System.arraycopy(files, 0, newValue, 0, files.length);
return newValue;
} }
public GHCompare wrap(GHRepository owner) { public GHCompare wrap(GHRepository owner) {

View File

@@ -78,7 +78,7 @@ public class GHContent {
*/ */
@SuppressFBWarnings("DM_DEFAULT_ENCODING") @SuppressFBWarnings("DM_DEFAULT_ENCODING")
public String getContent() throws IOException { public String getContent() throws IOException {
return new String(DatatypeConverter.parseBase64Binary(getEncodedContent())); return new String(Base64.decodeBase64(getEncodedContent()));
} }
/** /**
@@ -115,7 +115,8 @@ public class GHContent {
* Retrieves the actual content stored here. * Retrieves the actual content stored here.
*/ */
public InputStream read() throws IOException { public InputStream read() throws IOException {
return new Requester(root).asStream(getDownloadUrl()); // if the download link is encoded with a token on the query string, the default behavior of POST will fail
return new Requester(root).method("GET").asStream(getDownloadUrl());
} }
/** /**
@@ -178,7 +179,7 @@ public class GHContent {
} }
public GHContentUpdateResponse update(byte[] newContentBytes, String commitMessage, String branch) throws IOException { public GHContentUpdateResponse update(byte[] newContentBytes, String commitMessage, String branch) throws IOException {
String encodedContent = DatatypeConverter.printBase64Binary(newContentBytes); String encodedContent = Base64.encodeBase64String(newContentBytes);
Requester requester = new Requester(root) Requester requester = new Requester(root)
.with("path", path) .with("path", path)

View File

@@ -0,0 +1,21 @@
package org.kohsuke.github;
/**
* {@link GHContent} with license information.
*
* @author Kohsuke Kawaguchi
* @see <a href="https://developer.github.com/v3/licenses/#get-a-repositorys-license">documentation</a>
* @see GHRepository#getLicense()
*/
@Preview @Deprecated
class GHContentWithLicense extends GHContent {
GHLicense license;
@Override
GHContentWithLicense wrap(GHRepository owner) {
super.wrap(owner);
if (license!=null)
license.wrap(owner.root);
return this;
}
}

View File

@@ -5,10 +5,9 @@ import java.util.Locale;
/** /**
* Hook event type. * Hook event type.
* *
* See http://developer.github.com/v3/events/types/
*
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
* @see GHEventInfo * @see GHEventInfo
* @see <a href="https://developer.github.com/v3/activity/events/types/">Event type reference</a>
*/ */
public enum GHEvent { public enum GHEvent {
COMMIT_COMMENT, COMMIT_COMMENT,

View File

@@ -54,6 +54,7 @@ public class GHIssue extends GHObject {
protected int number; protected int number;
protected String closed_at; protected String closed_at;
protected int comments; protected int comments;
@SkipFromToString
protected String body; protected String body;
// for backward compatibility with < 1.63, this collection needs to hold instances of Label, not GHLabel // for backward compatibility with < 1.63, this collection needs to hold instances of Label, not GHLabel
protected List<Label> labels; protected List<Label> labels;

View File

@@ -0,0 +1,168 @@
/*
* The MIT License
*
* Copyright (c) 2016, Duncan Dickinson
*
* 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;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import static org.kohsuke.github.Previews.DRAX;
/**
* The GitHub Preview API's license information
* <p>
* WARNING: This uses a PREVIEW API - subject to change.
*
* @author Duncan Dickinson
* @see GitHub#getLicense(String)
* @see GHRepository#getLicense()
* @see <a href="https://developer.github.com/v3/licenses/">https://developer.github.com/v3/licenses/</a>
*/
@Preview @Deprecated
@SuppressWarnings({"UnusedDeclaration"})
@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD",
"NP_UNWRITTEN_FIELD"}, justification = "JSON API")
public class GHLicense extends GHObject {
@SuppressFBWarnings("IS2_INCONSISTENT_SYNC") // root is set before the object is returned to the app
/*package almost final*/ GitHub root;
// these fields are always present, even in the short form
protected String key, name;
// the rest is only after populated
protected Boolean featured;
protected String html_url, description, category, implementation, body;
protected List<String> required = new ArrayList<String>();
protected List<String> permitted = new ArrayList<String>();
protected List<String> forbidden = new ArrayList<String>();
/**
* @return a mnemonic for the license
*/
public String getKey() {
return key;
}
/**
* @return the license name
*/
public String getName() {
return name;
}
/**
* @return API URL of this object.
*/
@WithBridgeMethods(value = String.class, adapterMethod = "urlToString")
public URL getUrl() {
return GitHub.parseURL(url);
}
/**
* Featured licenses are bold in the new repository drop-down
*
* @return True if the license is featured, false otherwise
*/
public Boolean isFeatured() throws IOException {
populate();
return featured;
}
public URL getHtmlUrl() throws IOException {
populate();
return GitHub.parseURL(html_url);
}
public String getDescription() throws IOException {
populate();
return description;
}
public String getCategory() throws IOException {
populate();
return category;
}
public String getImplementation() throws IOException {
populate();
return implementation;
}
public List<String> getRequired() throws IOException {
populate();
return required;
}
public List<String> getPermitted() throws IOException {
populate();
return permitted;
}
public List<String> getForbidden() throws IOException {
populate();
return forbidden;
}
public String getBody() throws IOException {
populate();
return body;
}
/**
* Fully populate the data by retrieving missing data.
*
* Depending on the original API call where this object is created, it may not contain everything.
*/
protected synchronized void populate() throws IOException {
if (description!=null) return; // already populated
root.retrieve().withPreview(DRAX).to(url, this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GHLicense)) return false;
GHLicense that = (GHLicense) o;
return this.url.equals(that.url);
}
@Override
public int hashCode() {
return url.hashCode();
}
/*package*/ GHLicense wrap(GitHub root) {
this.root = root;
return this;
}
}

View File

@@ -72,12 +72,19 @@ public class GHMilestone extends GHObject {
} }
/** /**
* Closes this issue. * Closes this milestone.
*/ */
public void close() throws IOException { public void close() throws IOException {
edit("state", "closed"); edit("state", "closed");
} }
/**
* Reopens this milestone.
*/
public void reopen() throws IOException {
edit("state", "open");
}
private void edit(String key, Object value) throws IOException { private void edit(String key, Object value) throws IOException {
new Requester(root)._with(key, value).method("PATCH").to(getApiRoute()); new Requester(root)._with(key, value).method("PATCH").to(getApiRoute());
} }

View File

@@ -2,8 +2,13 @@ package org.kohsuke.github;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.lang.reflect.FieldUtils;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL; import java.net.URL;
import java.util.Date; import java.util.Date;
@@ -46,7 +51,7 @@ public abstract class GHObject {
* URL of this object for humans, which renders some HTML. * URL of this object for humans, which renders some HTML.
*/ */
@WithBridgeMethods(value=String.class, adapterMethod="urlToString") @WithBridgeMethods(value=String.class, adapterMethod="urlToString")
public abstract URL getHtmlUrl(); public abstract URL getHtmlUrl() throws IOException;
/** /**
* When was this resource last updated? * When was this resource last updated?
@@ -72,4 +77,39 @@ public abstract class GHObject {
private Object urlToString(URL url, Class type) { private Object urlToString(URL url, Class type) {
return url==null ? null : url.toString(); return url==null ? null : url.toString();
} }
/**
* String representation to assist debugging and inspection. The output format of this string
* is not a committed part of the API and is subject to change.
*/
@Override
public String toString() {
return new ReflectionToStringBuilder(this, TOSTRING_STYLE, null, null, false, false) {
@Override
protected boolean accept(Field field) {
return super.accept(field) && !field.isAnnotationPresent(SkipFromToString.class);
}
}.toString();
}
private static final ToStringStyle TOSTRING_STYLE = new ToStringStyle() {
{
this.setUseShortClassName(true);
}
@Override
public void append(StringBuffer buffer, String fieldName, Object value, Boolean fullDetail) {
// skip unimportant properties. '_' is a heuristics as important properties tend to have short names
if (fieldName.contains("_"))
return;
// avoid recursing other GHObject
if (value instanceof GHObject)
return;
// likewise no point in showing root
if (value instanceof GitHub)
return;
super.append(buffer,fieldName,value,fullDetail);
}
};
} }

View File

@@ -93,6 +93,17 @@ public class GHOrganization extends GHPerson {
return null; return null;
} }
/**
* Finds a team that has the given slug in its {@link GHTeam#getSlug()}
*/
public GHTeam getTeamBySlug(String slug) throws IOException {
for (GHTeam t : listTeams()) {
if(t.getSlug().equals(slug))
return t;
}
return null;
}
/** /**
* Checks if this organization has the specified user as a member. * Checks if this organization has the specified user as a member.
*/ */
@@ -209,7 +220,7 @@ public class GHOrganization extends GHPerson {
*/ */
public List<GHRepository> getRepositoriesWithOpenPullRequests() throws IOException { public List<GHRepository> getRepositoriesWithOpenPullRequests() throws IOException {
List<GHRepository> r = new ArrayList<GHRepository>(); List<GHRepository> r = new ArrayList<GHRepository>();
for (GHRepository repository : listRepositories()) { for (GHRepository repository : listRepositories(100)) {
repository.wrap(root); repository.wrap(root);
List<GHPullRequest> pullRequests = repository.getPullRequests(GHIssueState.OPEN); List<GHPullRequest> pullRequests = repository.getPullRequests(GHIssueState.OPEN);
if (pullRequests.size() > 0) { if (pullRequests.size() > 0) {

View File

@@ -52,7 +52,7 @@ public abstract class GHPerson extends GHObject {
*/ */
public synchronized Map<String,GHRepository> getRepositories() throws IOException { public synchronized Map<String,GHRepository> getRepositories() throws IOException {
Map<String,GHRepository> repositories = new TreeMap<String, GHRepository>(); Map<String,GHRepository> repositories = new TreeMap<String, GHRepository>();
for (GHRepository r : listRepositories()) { for (GHRepository r : listRepositories(100)) {
repositories.put(r.getName(),r); repositories.put(r.getName(),r);
} }
return Collections.unmodifiableMap(repositories); return Collections.unmodifiableMap(repositories);
@@ -204,7 +204,7 @@ public abstract class GHPerson extends GHObject {
public Date getUpdatedAt() throws IOException { public Date getUpdatedAt() throws IOException {
populate(); populate();
return super.getCreatedAt(); return super.getUpdatedAt();
} }
/** /**

View File

@@ -27,7 +27,6 @@ import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.net.URL; import java.net.URL;
import java.util.Arrays;
/** /**
* Commit detail inside a {@link GHPullRequest}. * Commit detail inside a {@link GHPullRequest}.
@@ -144,6 +143,8 @@ public class GHPullRequestCommitDetail {
} }
public CommitPointer[] getParents() { public CommitPointer[] getParents() {
return Arrays.copyOf(parents, parents.length); CommitPointer[] newValue = new CommitPointer[parents.length];
System.arraycopy(parents, 0, newValue, 0, parents.length);
return newValue;
} }
} }

View File

@@ -26,9 +26,9 @@ package org.kohsuke.github;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import javax.xml.bind.DatatypeConverter;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@@ -36,9 +36,22 @@ import java.io.InterruptedIOException;
import java.io.Reader; import java.io.Reader;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.kohsuke.github.Previews.DRAX;
/** /**
* A repository on GitHub. * A repository on GitHub.
@@ -53,6 +66,13 @@ public class GHRepository extends GHObject {
private String description, homepage, name, full_name; private String description, homepage, name, full_name;
private String html_url; // this is the UI private String html_url; // this is the UI
/*
* The license information makes use of the preview API.
*
* See: https://developer.github.com/v3/licenses/
*/
private GHLicense license;
private String git_url, ssh_url, clone_url, svn_url, mirror_url; private String git_url, ssh_url, clone_url, svn_url, mirror_url;
private GHUser owner; // not fully populated. beware. private GHUser owner; // not fully populated. beware.
private boolean has_issues, has_wiki, fork, has_downloads; private boolean has_issues, has_wiki, fork, has_downloads;
@@ -65,6 +85,7 @@ public class GHRepository extends GHObject {
private String default_branch,language; private String default_branch,language;
private Map<String,GHCommit> commits = new HashMap<String, GHCommit>(); private Map<String,GHCommit> commits = new HashMap<String, GHCommit>();
@SkipFromToString
private GHRepoPermission permissions; private GHRepoPermission permissions;
private GHRepository source, parent; private GHRepository source, parent;
@@ -574,7 +595,19 @@ public class GHRepository extends GHObject {
* Newly forked repository that belong to you. * Newly forked repository that belong to you.
*/ */
public GHRepository fork() throws IOException { public GHRepository fork() throws IOException {
return new Requester(root).method("POST").to(getApiTailUrl("forks"), GHRepository.class).wrap(root); new Requester(root).method("POST").to(getApiTailUrl("forks"), null);
// this API is asynchronous. we need to wait for a bit
for (int i=0; i<10; i++) {
GHRepository r = root.getMyself().getRepository(name);
if (r!=null) return r;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw (IOException)new InterruptedIOException().initCause(e);
}
}
throw new IOException(this+" was forked but can't find the new repository");
} }
/** /**
@@ -815,6 +848,46 @@ public class GHRepository extends GHObject {
}; };
} }
/**
* Gets the basic license details for the repository.
* <p>
* This is a preview item and subject to change.
*
* @throws IOException as usual but also if you don't use the preview connector
* @return null if there's no license.
*/
@Preview @Deprecated
public GHLicense getLicense() throws IOException{
GHContentWithLicense lic = getLicenseContent_();
return lic!=null ? lic.license : null;
}
/**
* Retrieves the contents of the repository's license file - makes an additional API call
* <p>
* This is a preview item and subject to change.
*
* @return details regarding the license contents, or null if there's no license.
* @throws IOException as usual but also if you don't use the preview connector
*/
@Preview @Deprecated
public GHContent getLicenseContent() throws IOException {
return getLicenseContent_();
}
@Preview @Deprecated
private GHContentWithLicense getLicenseContent_() throws IOException {
try {
return root.retrieve()
.withPreview(DRAX)
.to(getApiTailUrl("license"), GHContentWithLicense.class).wrap(this);
} catch (FileNotFoundException e) {
return null;
}
}
/**
/** /**
* Lists all the commit statues attached to the given commit, newer ones first. * Lists all the commit statues attached to the given commit, newer ones first.
*/ */
@@ -923,12 +996,36 @@ public class GHRepository extends GHObject {
} }
/** /**
* Lists all the users who have starred this repo. * Lists all the users who have starred this repo based on the old version of the API. For
* additional information, like date when the repository was starred, see {@link #listStargazers2()}
*/ */
public PagedIterable<GHUser> listStargazers() { public PagedIterable<GHUser> listStargazers() {
return listUsers("stargazers"); return listUsers("stargazers");
} }
/**
* Lists all the users who have starred this repo based on new version of the API, having extended
* information like the time when the repository was starred. For compatibility with the old API
* see {@link #listStargazers()}
*/
public PagedIterable<GHStargazer> listStargazers2() {
return new PagedIterable<GHStargazer>() {
@Override
public PagedIterator<GHStargazer> _iterator(int pageSize) {
Requester requester = root.retrieve();
requester.setHeader("Accept", "application/vnd.github.v3.star+json");
return new PagedIterator<GHStargazer>(requester.asIterator(getApiTailUrl("stargazers"), GHStargazer[].class, pageSize)) {
@Override
protected void wrapUp(GHStargazer[] page) {
for (GHStargazer c : page) {
c.wrapUp(GHRepository.this);
}
}
};
}
};
}
private PagedIterable<GHUser> listUsers(final String suffix) { private PagedIterable<GHUser> listUsers(final String suffix) {
return new PagedIterable<GHUser>() { return new PagedIterable<GHUser>() {
public PagedIterator<GHUser> _iterator(int pageSize) { public PagedIterator<GHUser> _iterator(int pageSize) {
@@ -997,6 +1094,7 @@ public class GHRepository extends GHObject {
*/ */
@SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS", @SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS",
justification = "It causes a performance degradation, but we have already exposed it to the API") justification = "It causes a performance degradation, but we have already exposed it to the API")
@SkipFromToString
private final Set<URL> postCommitHooks = new AbstractSet<URL>() { private final Set<URL> postCommitHooks = new AbstractSet<URL>() {
private List<URL> getPostCommitHooks() { private List<URL> getPostCommitHooks() {
try { try {
@@ -1066,6 +1164,10 @@ public class GHRepository extends GHObject {
return r; return r;
} }
public GHBranch getBranch(String name) throws IOException {
return root.retrieve().to(getApiTailUrl("branches/"+name),GHBranch.class).wrap(this);
}
/** /**
* @deprecated * @deprecated
* Use {@link #listMilestones(GHIssueState)} * Use {@link #listMilestones(GHIssueState)}
@@ -1152,7 +1254,7 @@ public class GHRepository extends GHObject {
try { try {
payload = content.getBytes("UTF-8"); payload = content.getBytes("UTF-8");
} catch (UnsupportedEncodingException ex) { } catch (UnsupportedEncodingException ex) {
throw new IOException("UTF-8 encoding is not supported", ex); throw (IOException) new IOException("UTF-8 encoding is not supported").initCause(ex);
} }
return createContent(payload, commitMessage, path, branch); return createContent(payload, commitMessage, path, branch);
} }
@@ -1165,7 +1267,7 @@ public class GHRepository extends GHObject {
Requester requester = new Requester(root) Requester requester = new Requester(root)
.with("path", path) .with("path", path)
.with("message", commitMessage) .with("message", commitMessage)
.with("content", DatatypeConverter.printBase64Binary(contentBytes)) .with("content", Base64.encodeBase64String(contentBytes))
.method("PUT"); .method("PUT");
if (branch != null) { if (branch != null) {
@@ -1314,14 +1416,9 @@ public class GHRepository extends GHObject {
} }
@Override
public String toString() {
return "Repository:"+owner.login+":"+name;
}
@Override @Override
public int hashCode() { public int hashCode() {
return toString().hashCode(); return ("Repository:"+owner.login+":"+name).hashCode();
} }
@Override @Override

View File

@@ -0,0 +1,51 @@
package org.kohsuke.github;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Date;
/**
* A stargazer at a repository on GitHub.
*
* @author noctarius
*/
@SuppressFBWarnings(value = {"UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD"}, justification = "JSON API")
public class GHStargazer {
private GHRepository repository;
private String starred_at;
private GHUser user;
/**
* Gets the repository that is stargazed
*
* @return the starred repository
*/
public GHRepository getRepository() {
return repository;
}
/**
* Gets the date when the repository was starred, however old stars before
* August 2012, will all show the date the API was changed to support starred_at.
*
* @return the date the stargazer was added
*/
public Date getStarredAt() {
return GitHub.parseDate(starred_at);
}
/**
* Gets the user that starred the repository
*
* @return the stargazer user
*/
public GHUser getUser() {
return user;
}
void wrapUp(GHRepository repository) {
this.repository = repository;
user.wrapUp(repository.root);
}
}

View File

@@ -12,7 +12,7 @@ import java.util.TreeMap;
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
public class GHTeam { public class GHTeam {
private String name,permission; private String name,permission,slug;
private int id; private int id;
private GHOrganization organization; // populated by GET /user/teams where Teams+Orgs are returned together private GHOrganization organization; // populated by GET /user/teams where Teams+Orgs are returned together
@@ -43,6 +43,10 @@ public class GHTeam {
return permission; return permission;
} }
public String getSlug() {
return slug;
}
public int getId() { public int getId() {
return id; return id;
} }
@@ -120,12 +124,25 @@ public class GHTeam {
} }
public void add(GHRepository r) throws IOException { public void add(GHRepository r) throws IOException {
org.root.retrieve().method("PUT").to(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null); add(r,null);
}
public void add(GHRepository r, GHOrganization.Permission permission) throws IOException {
org.root.retrieve().method("PUT")
.with("permission",permission)
.to(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null);
} }
public void remove(GHRepository r) throws IOException { public void remove(GHRepository r) throws IOException {
org.root.retrieve().method("DELETE").to(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null); org.root.retrieve().method("DELETE").to(api("/repos/" + r.getOwnerName() + '/' + r.getName()), null);
} }
/**
* Deletes this team.
*/
public void delete() throws IOException {
org.root.retrieve().method("DELETE").to(api(""));
}
private String api(String tail) { private String api(String tail) {
return "/teams/"+id+tail; return "/teams/"+id+tail;

View File

@@ -196,11 +196,6 @@ public class GHUser extends GHPerson {
}; };
} }
@Override
public String toString() {
return "User:"+login;
}
@Override @Override
public int hashCode() { public int hashCode() {
return login.hashCode(); return login.hashCode();

View File

@@ -25,12 +25,16 @@ package org.kohsuke.github;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; 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 java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.text.ParseException; import java.text.ParseException;
@@ -45,7 +49,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.apache.commons.codec.Charsets; import org.apache.commons.codec.Charsets;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
@@ -54,7 +57,8 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std; import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import java.nio.charset.Charset;
import java.util.logging.Logger;
/** /**
* Root of the GitHub API. * Root of the GitHub API.
@@ -81,6 +85,7 @@ public class GitHub {
private final String apiUrl; private final String apiUrl;
/*package*/ final RateLimitHandler rateLimitHandler; /*package*/ final RateLimitHandler rateLimitHandler;
/*package*/ final AbuseLimitHandler abuseLimitHandler;
private HttpConnector connector = HttpConnector.DEFAULT; private HttpConnector connector = HttpConnector.DEFAULT;
@@ -120,7 +125,7 @@ public class GitHub {
* @param connector * @param connector
* HttpConnector to use. Pass null to use default connector. * HttpConnector to use. Pass null to use default connector.
*/ */
/* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector, RateLimitHandler rateLimitHandler) throws IOException { /* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector, RateLimitHandler rateLimitHandler, AbuseLimitHandler abuseLimitHandler) throws IOException {
if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize
this.apiUrl = apiUrl; this.apiUrl = apiUrl;
if (null != connector) this.connector = connector; if (null != connector) this.connector = connector;
@@ -130,14 +135,15 @@ public class GitHub {
} else { } else {
if (password!=null) { if (password!=null) {
String authorization = (login + ':' + password); String authorization = (login + ':' + password);
Charset charset = Charsets.UTF_8; String charsetName = Charsets.UTF_8.name();
encodedAuthorization = "Basic "+new String(Base64.encodeBase64(authorization.getBytes(charset)), charset); encodedAuthorization = "Basic "+new String(Base64.encodeBase64(authorization.getBytes(charsetName)), charsetName);
} else {// anonymous access } else {// anonymous access
encodedAuthorization = null; encodedAuthorization = null;
} }
} }
this.rateLimitHandler = rateLimitHandler; this.rateLimitHandler = rateLimitHandler;
this.abuseLimitHandler = abuseLimitHandler;
if (login==null && encodedAuthorization!=null) if (login==null && encodedAuthorization!=null)
login = getMyself().getLogin(); login = getMyself().getLogin();
@@ -261,7 +267,8 @@ public class GitHub {
// see issue #78 // see issue #78
GHRateLimit r = new GHRateLimit(); GHRateLimit r = new GHRateLimit();
r.limit = r.remaining = 1000000; r.limit = r.remaining = 1000000;
r.reset = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); long hours = 1000L * 60 * 60;
r.reset = new Date(System.currentTimeMillis() + 1 * hours );
return r; return r;
} }
} }
@@ -334,6 +341,46 @@ public class GitHub {
String[] tokens = name.split("/"); String[] tokens = name.split("/");
return retrieve().to("/repos/" + tokens[0] + '/' + tokens[1], GHRepository.class).wrap(this); return retrieve().to("/repos/" + tokens[0] + '/' + tokens[1], GHRepository.class).wrap(this);
} }
/**
* Returns a list of popular open source licenses
*
* WARNING: This uses a PREVIEW API.
*
* @see <a href="https://developer.github.com/v3/licenses/">GitHub API - Licenses</a>
*
* @return a list of popular open source licenses
*/
@Preview @Deprecated
public PagedIterable<GHLicense> listLicenses() throws IOException {
return new PagedIterable<GHLicense>() {
public PagedIterator<GHLicense> _iterator(int pageSize) {
return new PagedIterator<GHLicense>(retrieve().withPreview(DRAX).asIterator("/licenses", GHLicense[].class, pageSize)) {
@Override
protected void wrapUp(GHLicense[] page) {
for (GHLicense c : page)
c.wrap(GitHub.this);
}
};
}
};
}
/**
* Returns the full details for a license
*
* WARNING: This uses a PREVIEW API.
*
* @param key The license key provided from the API
* @return The license details
* @throws IOException
* @see GHLicense#getKey()
*/
@Preview @Deprecated
public GHLicense getLicense(String key) throws IOException {
return retrieve().withPreview(DRAX).to("/licenses/" + key, GHLicense.class);
}
/**
/** /**
* This method returns a shallowly populated organizations. * This method returns a shallowly populated organizations.
@@ -458,6 +505,8 @@ public class GitHub {
retrieve().to("/user", GHUser.class); retrieve().to("/user", GHUser.class);
return true; return true;
} catch (IOException e) { } catch (IOException e) {
if (LOGGER.isLoggable(FINE))
LOGGER.log(FINE, "Exception validating credentials on " + this.apiUrl + " with login '" + this.login + "' " + e, e);
return false; return false;
} }
} }
@@ -475,14 +524,59 @@ public class GitHub {
} }
/** /**
* Ensures that the API URL is valid. * Tests the connection.
*
* <p>
* Verify that the API URL and credentials are valid to access this GitHub.
* *
* <p> * <p>
* This method returns normally if the endpoint is reachable and verified to be GitHub API URL. * 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. * Otherwise this method throws {@link IOException} to indicate the problem.
*/ */
public void checkApiUrlValidity() throws IOException { public void checkApiUrlValidity() throws IOException {
retrieve().to("/", GHApiInfo.class).check(apiUrl); try {
retrieve().to("/", GHApiInfo.class).check(apiUrl);
} catch (IOException e) {
if (isPrivateModeEnabled()) {
throw (IOException)new IOException("GitHub Enterprise server (" + apiUrl + ") with private mode enabled").initCause(e);
}
throw e;
}
}
/**
* Ensures if a GitHub Enterprise server is configured in private mode.
*
* @return {@code true} if private mode is enabled. If it tries to use this method with GitHub, returns {@code
* false}.
*/
private boolean isPrivateModeEnabled() {
try {
HttpURLConnection uc = getConnector().connect(getApiURL("/"));
/*
$ curl -i https://github.mycompany.com/api/v3/
HTTP/1.1 401 Unauthorized
Server: GitHub.com
Date: Sat, 05 Mar 2016 19:45:01 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 130
Status: 401 Unauthorized
X-GitHub-Media-Type: github.v3
X-XSS-Protection: 1; mode=block
X-Frame-Options: deny
Content-Security-Policy: default-src 'none'
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
Access-Control-Allow-Origin: *
X-GitHub-Request-Id: dbc70361-b11d-4131-9a7f-674b8edd0411
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
X-Content-Type-Options: nosniff
*/
return uc.getResponseCode() == HTTP_UNAUTHORIZED
&& uc.getHeaderField("X-GitHub-Media-Type") != null;
} catch (IOException e) {
return false;
}
} }
/** /**
@@ -604,4 +698,6 @@ public class GitHub {
} }
/* package */ static final String GITHUB_URL = "https://api.github.com"; /* package */ static final String GITHUB_URL = "https://api.github.com";
private static final Logger LOGGER = Logger.getLogger(GitHub.class.getName());
} }

View File

@@ -15,7 +15,7 @@ import java.util.Map.Entry;
import java.util.Properties; import java.util.Properties;
/** /**
* * Configures connection details and produces {@link GitHub}.
* *
* @since 1.59 * @since 1.59
*/ */
@@ -30,6 +30,7 @@ public class GitHubBuilder {
private HttpConnector connector; private HttpConnector connector;
private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT; private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
private AbuseLimitHandler abuseLimitHandler = AbuseLimitHandler.WAIT;
public GitHubBuilder() { public GitHubBuilder() {
} }
@@ -178,6 +179,10 @@ public class GitHubBuilder {
this.rateLimitHandler = handler; this.rateLimitHandler = handler;
return this; return this;
} }
public GitHubBuilder withAbuseLimitHandler(AbuseLimitHandler handler) {
this.abuseLimitHandler = handler;
return this;
}
/** /**
* Configures {@linkplain #withConnector(HttpConnector) connector} * Configures {@linkplain #withConnector(HttpConnector) connector}
@@ -193,6 +198,6 @@ public class GitHubBuilder {
} }
public GitHub build() throws IOException { public GitHub build() throws IOException {
return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler); return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler, abuseLimitHandler);
} }
} }

View File

@@ -0,0 +1,118 @@
package org.kohsuke.github;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.annotation.CheckForNull;
/**
* {@link IOException} for http exceptions because {@link HttpURLConnection} throws un-discerned
* {@link IOException} and it can help to know the http response code to decide how to handle an
* http exceptions.
*
* @author <a href="mailto:cleclerc@cloudbees.com">Cyrille Le Clerc</a>
*/
public class HttpException extends IOException {
static final long serialVersionUID = 1L;
private final int responseCode;
private final String responseMessage;
private final String url;
/**
* @param message The detail message (which is saved for later retrieval
* by the {@link #getMessage()} method)
* @param responseCode Http response code. {@code -1} if no code can be discerned.
* @param responseMessage Http response message
* @param url The url that was invoked
* @see HttpURLConnection#getResponseCode()
* @see HttpURLConnection#getResponseMessage()
*/
public HttpException(String message, int responseCode, String responseMessage, String url) {
super(message);
this.responseCode = responseCode;
this.responseMessage = responseMessage;
this.url = url;
}
/**
* @param message The detail message (which is saved for later retrieval
* by the {@link #getMessage()} method)
* @param responseCode Http response code. {@code -1} if no code can be discerned.
* @param responseMessage Http response message
* @param url The url that was invoked
* @param cause The cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A null value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @see HttpURLConnection#getResponseCode()
* @see HttpURLConnection#getResponseMessage()
*/
public HttpException(String message, int responseCode, String responseMessage, String url, Throwable cause) {
super(message);
initCause(cause);
this.responseCode = responseCode;
this.responseMessage = responseMessage;
this.url = url;
}
/**
* @param responseCode Http response code. {@code -1} if no code can be discerned.
* @param responseMessage Http response message
* @param url The url that was invoked
* @param cause The cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A null value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @see HttpURLConnection#getResponseCode()
* @see HttpURLConnection#getResponseMessage()
*/
public HttpException(int responseCode, String responseMessage, String url, Throwable cause) {
super("Server returned HTTP response code: " + responseCode + ", message: '" + responseMessage + "'" +
" for URL: " + url);
initCause(cause);
this.responseCode = responseCode;
this.responseMessage = responseMessage;
this.url = url;
}
/**
* @param responseCode Http response code. {@code -1} if no code can be discerned.
* @param responseMessage Http response message
* @param url The url that was invoked
* @param cause The cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A null value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @see HttpURLConnection#getResponseCode()
* @see HttpURLConnection#getResponseMessage()
*/
public HttpException(int responseCode, String responseMessage, @CheckForNull URL url, Throwable cause) {
this(responseCode, responseMessage, url == null ? null : url.toString(), cause);
}
/**
* Http response code of the request that cause the exception
*
* @return {@code -1} if no code can be discerned.
*/
public int getResponseCode() {
return responseCode;
}
/**
* Http response message of the request that cause the exception
*
* @return {@code null} if no response message can be discerned.
*/
public String getResponseMessage() {
return responseMessage;
}
/**
* The http URL that caused the exception
*
* @return url
*/
public String getUrl() {
return url;
}
}

View File

@@ -0,0 +1,18 @@
package org.kohsuke.github;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Indicates that the method/class/etc marked maps to GitHub API in the preview period.
*
* These APIs are subject to change and not a part of the backward compatibility commitment.
* Always used in conjunction with 'deprecated' to raise awareness to clients.
*
* @author Kohsuke Kawaguchi
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Preview {
}

View File

@@ -0,0 +1,9 @@
package org.kohsuke.github;
/**
* @author Kohsuke Kawaguchi
*/
/*package*/ class Previews {
static final String LOKI = "application/vnd.github.loki-preview+json";
static final String DRAX = "application/vnd.github.drax-preview+json";
}

View File

@@ -9,6 +9,7 @@ import java.net.HttpURLConnection;
* *
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
* @see GitHubBuilder#withRateLimitHandler(RateLimitHandler) * @see GitHubBuilder#withRateLimitHandler(RateLimitHandler)
* @see AbuseLimitHandler
*/ */
public abstract class RateLimitHandler { public abstract class RateLimitHandler {
/** /**

View File

@@ -24,6 +24,7 @@
package org.kohsuke.github; package org.kohsuke.github;
import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonMappingException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@@ -48,6 +49,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.logging.Logger;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
@@ -55,6 +57,7 @@ import java.util.zip.GZIPInputStream;
import javax.annotation.WillClose; import javax.annotation.WillClose;
import static java.util.Arrays.asList; 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.*;
/** /**
@@ -63,8 +66,6 @@ import static org.kohsuke.github.GitHub.*;
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
class Requester { class Requester {
private static final List<String> METHODS_WITHOUT_BODY = asList("GET", "DELETE");
private final GitHub root; private final GitHub root;
private final List<Entry> args = new ArrayList<Entry>(); private final List<Entry> args = new ArrayList<Entry>();
private final Map<String,String> headers = new LinkedHashMap<String, String>(); private final Map<String,String> headers = new LinkedHashMap<String, String>();
@@ -104,6 +105,15 @@ class Requester {
headers.put(name,value); headers.put(name,value);
} }
public Requester withHeader(String name, String value) {
setHeader(name,value);
return this;
}
/*package*/ Requester withPreview(String name) {
return withHeader("Accept",name);
}
/** /**
* Makes a request with authentication credential. * Makes a request with authentication credential.
*/ */
@@ -137,7 +147,7 @@ class Requester {
// by convention Java constant names are upper cases, but github uses // by convention Java constant names are upper cases, but github uses
// lower-case constants. GitHub also uses '-', which in Java we always // lower-case constants. GitHub also uses '-', which in Java we always
// replace by '_' // replace by '_'
return with(key, e.toString().toLowerCase(Locale.ENGLISH).replace('_','-')); return with(key, e.toString().toLowerCase(Locale.ENGLISH).replace('_', '-'));
} }
public Requester with(String key, String value) { public Requester with(String key, String value) {
@@ -215,19 +225,24 @@ class Requester {
*/ */
@Deprecated @Deprecated
public <T> T to(String tailApiUrl, Class<T> type, String method) throws IOException { public <T> T to(String tailApiUrl, Class<T> type, String method) throws IOException {
return method(method).to(tailApiUrl,type); return method(method).to(tailApiUrl, type);
} }
@SuppressFBWarnings("SBSC_USE_STRINGBUFFER_CONCATENATION")
private <T> T _to(String tailApiUrl, Class<T> type, T instance) throws IOException { private <T> T _to(String tailApiUrl, Class<T> type, T instance) throws IOException {
while (true) {// loop while API rate limit is hit if (METHODS_WITHOUT_BODY.contains(method) && !args.isEmpty()) {
if (METHODS_WITHOUT_BODY.contains(method) && !args.isEmpty()) { boolean questionMarkFound = tailApiUrl.indexOf('?') != -1;
StringBuilder qs=new StringBuilder(); tailApiUrl += questionMarkFound ? '&' : '?';
for (Entry arg : args) { for (Iterator<Entry> it = args.listIterator(); it.hasNext();) {
qs.append(qs.length()==0 ? '?' : '&'); Entry arg = it.next();
qs.append(arg.key).append('=').append(URLEncoder.encode(arg.value.toString(),"UTF-8")); tailApiUrl += arg.key + '=' + URLEncoder.encode(arg.value.toString(),"UTF-8");
if (it.hasNext()) {
tailApiUrl += '&';
} }
tailApiUrl += qs.toString();
} }
}
while (true) {// loop while API rate limit is hit
setupConnection(root.getApiURL(tailApiUrl)); setupConnection(root.getApiURL(tailApiUrl));
buildRequest(); buildRequest();
@@ -264,10 +279,9 @@ class Requester {
*/ */
public int asHttpStatusCode(String tailApiUrl) throws IOException { public int asHttpStatusCode(String tailApiUrl) throws IOException {
while (true) {// loop while API rate limit is hit while (true) {// loop while API rate limit is hit
method("GET");
setupConnection(root.getApiURL(tailApiUrl)); setupConnection(root.getApiURL(tailApiUrl));
uc.setRequestMethod("GET");
buildRequest(); buildRequest();
try { try {
@@ -282,10 +296,7 @@ class Requester {
while (true) {// loop while API rate limit is hit while (true) {// loop while API rate limit is hit
setupConnection(root.getApiURL(tailApiUrl)); setupConnection(root.getApiURL(tailApiUrl));
// if the download link is encoded with a token on the query string, the default behavior of POST will fail buildRequest();
uc.setRequestMethod("GET");
buildRequest();
try { try {
return wrapStream(uc.getInputStream()); return wrapStream(uc.getInputStream());
@@ -460,6 +471,11 @@ class Requester {
uc.setRequestProperty(e.getKey(), v); uc.setRequestProperty(e.getKey(), v);
} }
setRequestMethod(uc);
uc.setRequestProperty("Accept-Encoding", "gzip");
}
private void setRequestMethod(HttpURLConnection uc) throws IOException {
try { try {
uc.setRequestMethod(method); uc.setRequestMethod(method);
} catch (ProtocolException e) { } catch (ProtocolException e) {
@@ -471,26 +487,57 @@ class Requester {
} catch (Exception x) { } catch (Exception x) {
throw (IOException)new IOException("Failed to set the custom verb").initCause(x); throw (IOException)new IOException("Failed to set the custom verb").initCause(x);
} }
// sun.net.www.protocol.https.DelegatingHttpsURLConnection delegates to another HttpURLConnection
try {
Field $delegate = uc.getClass().getDeclaredField("delegate");
$delegate.setAccessible(true);
Object delegate = $delegate.get(uc);
if (delegate instanceof HttpURLConnection) {
HttpURLConnection nested = (HttpURLConnection) delegate;
setRequestMethod(nested);
}
} catch (NoSuchFieldException x) {
// no problem
} catch (IllegalAccessException x) {
throw (IOException)new IOException("Failed to set the custom verb").initCause(x);
}
} }
uc.setRequestProperty("Accept-Encoding", "gzip"); if (!uc.getRequestMethod().equals(method))
throw new IllegalStateException("Failed to set the request method to "+method);
} }
private <T> T parse(Class<T> type, T instance) throws IOException { private <T> T parse(Class<T> type, T instance) throws IOException {
if (uc.getResponseCode()==304)
return null; // special case handling for 304 unmodified, as the content will be ""
InputStreamReader r = null; InputStreamReader r = null;
int responseCode = -1;
String responseMessage = null;
try { try {
responseCode = uc.getResponseCode();
responseMessage = uc.getResponseMessage();
if (responseCode == 304) {
return null; // special case handling for 304 unmodified, as the content will be ""
}
if (responseCode == 204 && type!=null && type.isArray()) {
// no content
return type.cast(Array.newInstance(type.getComponentType(),0));
}
r = new InputStreamReader(wrapStream(uc.getInputStream()), "UTF-8"); r = new InputStreamReader(wrapStream(uc.getInputStream()), "UTF-8");
String data = IOUtils.toString(r); String data = IOUtils.toString(r);
if (type!=null) if (type!=null)
try { try {
return MAPPER.readValue(data,type); return MAPPER.readValue(data,type);
} catch (JsonMappingException e) { } catch (JsonMappingException e) {
throw (IOException)new IOException("Failed to deserialize "+data).initCause(e); throw (IOException)new IOException("Failed to deserialize " +data).initCause(e);
} }
if (instance!=null) if (instance!=null)
return MAPPER.readerForUpdating(instance).<T>readValue(data); return MAPPER.readerForUpdating(instance).<T>readValue(data);
return null; return null;
} catch (FileNotFoundException e) {
// java.net.URLConnection handles 404 exception has FileNotFoundException, don't wrap exception in HttpException
// to preserve backward compatibility
throw e;
} catch (IOException e) {
throw new HttpException(responseCode, responseMessage, uc.getURL(), e);
} finally { } finally {
IOUtils.closeQuietly(r); IOUtils.closeQuietly(r);
} }
@@ -511,7 +558,18 @@ class Requester {
* Handle API error by either throwing it or by returning normally to retry. * Handle API error by either throwing it or by returning normally to retry.
*/ */
/*package*/ void handleApiError(IOException e) throws IOException { /*package*/ void handleApiError(IOException e) throws IOException {
if (uc.getResponseCode() == 401) // Unauthorized == bad creds int responseCode;
try {
responseCode = uc.getResponseCode();
} catch (IOException e2) {
// likely to be a network exception (e.g. SSLHandshakeException),
// uc.getResponseCode() and any other getter on the response will cause an exception
if (LOGGER.isLoggable(FINE))
LOGGER.log(FINE, "Silently ignore exception retrieving response code for '" + uc.getURL() + "'" +
" handling exception " + e, e);
throw e;
}
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) // 401 / Unauthorized == bad creds
throw e; throw e;
if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) { if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) {
@@ -519,6 +577,13 @@ class Requester {
return; return;
} }
// Retry-After is not documented but apparently that field exists
if (responseCode == HttpURLConnection.HTTP_FORBIDDEN &&
uc.getHeaderField("Retry-After") != null) {
this.root.abuseLimitHandler.onError(e,uc);
return;
}
InputStream es = wrapStream(uc.getErrorStream()); InputStream es = wrapStream(uc.getErrorStream());
try { try {
if (es!=null) { if (es!=null) {
@@ -533,4 +598,7 @@ class Requester {
IOUtils.closeQuietly(es); IOUtils.closeQuietly(es);
} }
} }
private static final List<String> METHODS_WITHOUT_BODY = asList("GET", "DELETE");
private static final Logger LOGGER = Logger.getLogger(Requester.class.getName());
} }

View File

@@ -0,0 +1,16 @@
package org.kohsuke.github;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Ignores this field for {@link GHObject#toString()}
*
* @author Kohsuke Kawaguchi
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface SkipFromToString {
}

View File

@@ -1,4 +1,5 @@
import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHRepository.Contributor;
import org.kohsuke.github.GHUser; import org.kohsuke.github.GHUser;
import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHub;
@@ -9,11 +10,10 @@ import java.util.Collection;
*/ */
public class Foo { public class Foo {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
Collection<GHRepository> lst = GitHub.connect().getUser("kohsuke").getRepositories().values(); GitHub gh = GitHub.connect();
for (GHRepository r : lst) { for (Contributor c : gh.getRepository("kohsuke/yo").listContributors()) {
System.out.println(r.getName()); System.out.println(c);
} }
System.out.println(lst.size());
} }
private static void testRateLimit() throws Exception { private static void testRateLimit() throws Exception {

View File

@@ -14,6 +14,7 @@ import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
@@ -39,7 +40,7 @@ public class AppTest extends AbstractGitHubApiTestBase {
} }
@Test @Test
public void testRepositoryWithAutoInitializationCRUD() throws IOException { public void testRepositoryWithAutoInitializationCRUD() throws Exception {
String name = "github-api-test-autoinit"; String name = "github-api-test-autoinit";
deleteRepository(name); deleteRepository(name);
GHRepository r = gitHub.createRepository(name) GHRepository r = gitHub.createRepository(name)
@@ -49,6 +50,7 @@ public class AppTest extends AbstractGitHubApiTestBase {
r.enableIssueTracker(false); r.enableIssueTracker(false);
r.enableDownloads(false); r.enableDownloads(false);
r.enableWiki(false); r.enableWiki(false);
Thread.sleep(3000);
assertNotNull(r.getReadme()); assertNotNull(r.getReadme());
getUser().getRepository(name).delete(); getUser().getRepository(name).delete();
} }
@@ -221,10 +223,12 @@ public class AppTest extends AbstractGitHubApiTestBase {
} }
@Test @Test
public void testMyTeamsContainsAllMyOrganizations() throws IOException { public void testMyOrganizationsContainMyTeams() throws IOException {
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams(); Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
Map<String, GHOrganization> myOrganizations = gitHub.getMyOrganizations(); Map<String, GHOrganization> myOrganizations = gitHub.getMyOrganizations();
assertEquals(teams.keySet(), myOrganizations.keySet()); //GitHub no longer has default 'owners' team, so there may be organization memberships without a team
//https://help.github.com/articles/about-improved-organization-permissions/
assertTrue(myOrganizations.keySet().containsAll(teams.keySet()));
} }
@Test @Test
@@ -335,6 +339,13 @@ public class AppTest extends AbstractGitHubApiTestBase {
assertNotNull(e); assertNotNull(e);
} }
@Test
public void testOrgTeamBySlug() throws Exception {
kohsuke();
GHTeam e = gitHub.getOrganization("github-api-test-org").getTeamBySlug("core-developers");
assertNotNull(e);
}
@Test @Test
public void testCommit() throws Exception { public void testCommit() throws Exception {
GHCommit commit = gitHub.getUser("jenkinsci").getRepository("jenkins").getCommit("08c1c9970af4d609ae754fbe803e06186e3206f7"); GHCommit commit = gitHub.getUser("jenkinsci").getRepository("jenkins").getCommit("08c1c9970af4d609ae754fbe803e06186e3206f7");
@@ -592,6 +603,8 @@ public class AppTest extends AbstractGitHubApiTestBase {
.prerelease(false) .prerelease(false)
.create(); .create();
Thread.sleep(3000);
try { try {
for (GHTag tag : r.listTags()) { for (GHTag tag : r.listTags()) {
@@ -845,6 +858,18 @@ public class AppTest extends AbstractGitHubApiTestBase {
gitHub.listNotifications().markAsRead(); gitHub.listNotifications().markAsRead();
} }
/**
* Just basic code coverage to make sure toString() doesn't blow up
*/
@Test
public void checkToString() throws Exception {
GHUser u = gitHub.getUser("rails");
System.out.println(u);
GHRepository r = u.getRepository("rails");
System.out.println(r);
System.out.println(r.getIssue(1));
}
private void kohsuke() { private void kohsuke() {
String login = getUser().getLogin(); String login = getUser().getLogin();
Assume.assumeTrue(login.equals("kohsuke") || login.equals("kohsuke2")); Assume.assumeTrue(login.equals("kohsuke") || login.equals("kohsuke2"));

View File

@@ -20,6 +20,13 @@ public class GHContentIntegrationTest extends AbstractGitHubApiTestBase {
repo = gitHub.getRepository("github-api-test-org/GHContentIntegrationTest").fork(); repo = gitHub.getRepository("github-api-test-org/GHContentIntegrationTest").fork();
} }
@Test
public void testBranchProtection() throws Exception {
GHBranch b = repo.getBranch("master");
b.enableProtection(EnforcementLevel.NON_ADMINS, "foo/bar");
b.disableProtection();
}
@Test @Test
public void testGetFileContent() throws Exception { public void testGetFileContent() throws Exception {
GHContent content = repo.getFileContent("ghcontent-ro/a-file-with-content"); GHContent content = repo.getFileContent("ghcontent-ro/a-file-with-content");
@@ -66,7 +73,8 @@ public class GHContentIntegrationTest extends AbstractGitHubApiTestBase {
assertNotNull(updatedContentResponse.getCommit()); assertNotNull(updatedContentResponse.getCommit());
assertNotNull(updatedContentResponse.getContent()); assertNotNull(updatedContentResponse.getContent());
assertEquals("this is some new content\n", updatedContent.getContent()); // due to what appears to be a cache propagation delay, this test is too flaky
// assertEquals("this is some new content\n", updatedContent.getContent());
GHContentUpdateResponse deleteResponse = updatedContent.delete("Enough of this foolishness!"); GHContentUpdateResponse deleteResponse = updatedContent.delete("Enough of this foolishness!");

View File

@@ -0,0 +1,193 @@
/*
* The MIT License
*
* Copyright (c) 2016, Duncan Dickinson
*
* 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;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.net.URL;
/**
* @author Duncan Dickinson
*/
public class GHLicenseTest extends Assert {
private GitHub gitHub;
@Before
public void setUp() throws Exception {
gitHub = new GitHubBuilder()
.fromCredentials()
.build();
}
/**
* Basic test to ensure that the list of licenses from {@link GitHub#listLicenses()} is returned
*
* @throws IOException
*/
@Test
public void listLicenses() throws IOException {
Iterable<GHLicense> licenses = gitHub.listLicenses();
assertTrue(licenses.iterator().hasNext());
}
/**
* Tests that {@link GitHub#listLicenses()} returns the MIT license
* in the expected manner.
*
* @throws IOException
*/
@Test
public void listLicensesCheckIndividualLicense() throws IOException {
PagedIterable<GHLicense> licenses = gitHub.listLicenses();
for (GHLicense lic : licenses) {
if (lic.getKey().equals("mit")) {
assertTrue(lic.getUrl().equals(new URL("https://api.github.com/licenses/mit")));
return;
}
}
fail("The MIT license was not found");
}
/**
* Checks that the request for an individual license using {@link GitHub#getLicense(String)}
* returns expected values (not all properties are checked)
*
* @throws IOException
*/
@Test
public void getLicense() throws IOException {
String key = "mit";
GHLicense license = gitHub.getLicense(key);
assertNotNull(license);
assertTrue("The name is correct", license.getName().equals("MIT License"));
assertTrue("The HTML URL is correct", license.getHtmlUrl().equals(new URL("http://choosealicense.com/licenses/mit/")));
}
/**
* Accesses the 'kohsuke/github-api' repo using {@link GitHub#getRepository(String)}
* and checks that the license is correct
*
* @throws IOException
*/
@Test
public void checkRepositoryLicense() throws IOException {
GHRepository repo = gitHub.getRepository("kohsuke/github-api");
GHLicense license = repo.getLicense();
assertNotNull("The license is populated", license);
assertTrue("The key is correct", license.getKey().equals("mit"));
assertTrue("The name is correct", license.getName().equals("MIT License"));
assertTrue("The URL is correct", license.getUrl().equals(new URL("https://api.github.com/licenses/mit")));
}
/**
* Accesses the 'atom/atom' repo using {@link GitHub#getRepository(String)}
* and checks that the license is correct
*
* @throws IOException
*/
@Test
public void checkRepositoryLicenseAtom() throws IOException {
GHRepository repo = gitHub.getRepository("atom/atom");
GHLicense license = repo.getLicense();
assertNotNull("The license is populated", license);
assertTrue("The key is correct", license.getKey().equals("mit"));
assertTrue("The name is correct", license.getName().equals("MIT License"));
assertTrue("The URL is correct", license.getUrl().equals(new URL("https://api.github.com/licenses/mit")));
}
/**
* Accesses the 'pomes/pomes' repo using {@link GitHub#getRepository(String)}
* and checks that the license is correct
*
* @throws IOException
*/
@Test
public void checkRepositoryLicensePomes() throws IOException {
GHRepository repo = gitHub.getRepository("pomes/pomes");
GHLicense license = repo.getLicense();
assertNotNull("The license is populated", license);
assertTrue("The key is correct", license.getKey().equals("apache-2.0"));
assertTrue("The name is correct", license.getName().equals("Apache License 2.0"));
assertTrue("The URL is correct", license.getUrl().equals(new URL("https://api.github.com/licenses/apache-2.0")));
}
/**
* Accesses the 'dedickinson/test-repo' repo using {@link GitHub#getRepository(String)}
* and checks that *no* license is returned as the repo doesn't have one
*
* @throws IOException
*/
@Test
public void checkRepositoryWithoutLicense() throws IOException {
GHRepository repo = gitHub.getRepository("dedickinson/test-repo");
GHLicense license = repo.getLicense();
assertNull("There is no license", license);
}
/**
* Accesses the 'kohsuke/github-api' repo using {@link GitHub#getRepository(String)}
* and then calls {@link GHRepository#getLicense()} and checks that certain
* properties are correct
*
* @throws IOException
*/
@Test
public void checkRepositoryFullLicense() throws IOException {
GHRepository repo = gitHub.getRepository("kohsuke/github-api");
GHLicense license = repo.getLicense();
assertNotNull("The license is populated", license);
assertTrue("The key is correct", license.getKey().equals("mit"));
assertTrue("The name is correct", license.getName().equals("MIT License"));
assertTrue("The URL is correct", license.getUrl().equals(new URL("https://api.github.com/licenses/mit")));
assertTrue("The HTML URL is correct", license.getHtmlUrl().equals(new URL("http://choosealicense.com/licenses/mit/")));
}
/**
* Accesses the 'pomes/pomes' repo using {@link GitHub#getRepository(String)}
* and then calls {@link GHRepository#getLicenseContent()} and checks that certain
* properties are correct
*
* @throws IOException
*/
@Test
public void checkRepositoryLicenseContent() throws IOException {
GHRepository repo = gitHub.getRepository("pomes/pomes");
GHContent content = repo.getLicenseContent();
assertNotNull("The license content is populated", content);
assertTrue("The type is 'file'", content.getType().equals("file"));
assertTrue("The license file is 'LICENSE'", content.getName().equals("LICENSE"));
if (content.getEncoding().equals("base64")) {
String licenseText = new String(IOUtils.toByteArray(content.read()));
assertTrue("The license appears to be an Apache License", licenseText.contains("Apache License"));
} else {
fail("Expected the license to be Base64 encoded but instead it was " + content.getEncoding());
}
}
}

View File

@@ -124,6 +124,11 @@ public class GitHubTest {
@Test @Test
public void testGitHubIsApiUrlValid() throws IOException { public void testGitHubIsApiUrlValid() throws IOException {
GitHub github = GitHub.connectAnonymously(); GitHub github = GitHub.connectAnonymously();
github.checkApiUrlValidity(); //GitHub github = GitHub.connectToEnterpriseAnonymously("https://github.mycompany.com/api/v3/");
try {
github.checkApiUrlValidity();
} catch (IOException ioe) {
assertTrue(ioe.getMessage().contains("private mode enabled"));
}
} }
} }

View File

@@ -50,4 +50,12 @@ public class RepositoryTest extends AbstractGitHubApiTestBase {
String mainLanguage = r.getLanguage(); String mainLanguage = r.getLanguage();
assertTrue(r.listLanguages().containsKey(mainLanguage)); assertTrue(r.listLanguages().containsKey(mainLanguage));
} }
@Test // Issue #261
public void listEmptyContributors() throws IOException {
GitHub gh = GitHub.connect();
for (Contributor c : gh.getRepository("github-api-test-org/empty").listContributors()) {
System.out.println(c);
}
}
} }