mirror of
https://github.com/jlengrand/github-api.git
synced 2026-03-11 00:11:25 +00:00
Compare commits
52 Commits
github-api
...
github-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277ccb5188 | ||
|
|
73119afeff | ||
|
|
8939179be8 | ||
|
|
adba2e68db | ||
|
|
0ef8b471a3 | ||
|
|
205950fc5f | ||
|
|
8835b2c745 | ||
|
|
74fda40764 | ||
|
|
687a36937e | ||
|
|
2c7b8bd6e8 | ||
|
|
e9417f5fa1 | ||
|
|
5e08b34c43 | ||
|
|
7b436ffb3b | ||
|
|
1ee2ec3728 | ||
|
|
ed28768146 | ||
|
|
f931835176 | ||
|
|
0cf9bc2814 | ||
|
|
8b428f2c93 | ||
|
|
10238dbcd3 | ||
|
|
6229e0928d | ||
|
|
5c7b259fe9 | ||
|
|
cc84c867c0 | ||
|
|
5bf252e12d | ||
|
|
75512ff66a | ||
|
|
6f4832476a | ||
|
|
86b0d27299 | ||
|
|
5a8845f7f6 | ||
|
|
709e47f32f | ||
|
|
77590b4eb3 | ||
|
|
72fc313135 | ||
|
|
39b32cee2e | ||
|
|
bdcee7c052 | ||
|
|
4093e53b5b | ||
|
|
a4c1c8de24 | ||
|
|
7ed234c875 | ||
|
|
0359160ac6 | ||
|
|
2478dad9b5 | ||
|
|
690292352b | ||
|
|
271d18cddc | ||
|
|
e1465639e7 | ||
|
|
ce7ca59339 | ||
|
|
76610b25d7 | ||
|
|
dfce0bda7c | ||
|
|
41c0dd9727 | ||
|
|
232c0389d3 | ||
|
|
d95c8a4ab0 | ||
|
|
374fdb37e1 | ||
|
|
f78530636e | ||
|
|
0bf81f4fb9 | ||
|
|
dcc3b7f36b | ||
|
|
a716a59489 | ||
|
|
bad0d1bbcf |
6
pom.xml
6
pom.xml
@@ -3,11 +3,11 @@
|
||||
<parent>
|
||||
<groupId>org.kohsuke</groupId>
|
||||
<artifactId>pom</artifactId>
|
||||
<version>12</version>
|
||||
<version>14</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>github-api</artifactId>
|
||||
<version>1.63</version>
|
||||
<version>1.66</version>
|
||||
<name>GitHub API for Java</name>
|
||||
<url>http://github-api.kohsuke.org/</url>
|
||||
<description>GitHub API for Java</description>
|
||||
@@ -16,7 +16,7 @@
|
||||
<connection>scm:git:git@github.com/kohsuke/${project.artifactId}.git</connection>
|
||||
<developerConnection>scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git</developerConnection>
|
||||
<url>http://${project.artifactId}.kohsuke.org/</url>
|
||||
<tag>github-api-1.63</tag>
|
||||
<tag>github-api-1.66</tag>
|
||||
</scm>
|
||||
|
||||
<distributionManagement>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
|
||||
@@ -14,7 +16,13 @@ import javax.xml.bind.DatatypeConverter;
|
||||
*/
|
||||
@SuppressWarnings({"UnusedDeclaration"})
|
||||
public class GHContent {
|
||||
private GHRepository owner;
|
||||
/*
|
||||
In normal use of this class, repository field is set via wrap(),
|
||||
but in the code search API, there's a nested 'repository' field that gets populated from JSON.
|
||||
*/
|
||||
private GHRepository repository;
|
||||
|
||||
private GitHub root;
|
||||
|
||||
private String type;
|
||||
private String encoding;
|
||||
@@ -26,9 +34,10 @@ public class GHContent {
|
||||
private String url; // this is the API url
|
||||
private String git_url; // this is the Blob url
|
||||
private String html_url; // this is the UI
|
||||
private String download_url;
|
||||
|
||||
public GHRepository getOwner() {
|
||||
return owner;
|
||||
return repository;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
@@ -58,35 +67,34 @@ public class GHContent {
|
||||
/**
|
||||
* Retrieve the decoded content that is stored at this location.
|
||||
*
|
||||
* <p>
|
||||
* Due to the nature of GitHub's API, you're not guaranteed that
|
||||
* the content will already be populated, so this may trigger
|
||||
* network activity, and can throw an IOException.
|
||||
**/
|
||||
*
|
||||
* @deprecated
|
||||
* Use {@link #read()}
|
||||
*/
|
||||
public String getContent() throws IOException {
|
||||
return new String(DatatypeConverter.parseBase64Binary(getEncodedContent()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the raw content that is stored at this location.
|
||||
* Retrieve the base64-encoded content that is stored at this location.
|
||||
*
|
||||
* <p>
|
||||
* Due to the nature of GitHub's API, you're not guaranteed that
|
||||
* the content will already be populated, so this may trigger
|
||||
* network activity, and can throw an IOException.
|
||||
**/
|
||||
*
|
||||
* @deprecated
|
||||
* Use {@link #read()}
|
||||
*/
|
||||
public String getEncodedContent() throws IOException {
|
||||
if (content != null)
|
||||
if (content!=null)
|
||||
return content;
|
||||
|
||||
GHContent retrievedContent = owner.getFileContent(path);
|
||||
|
||||
this.size = retrievedContent.size;
|
||||
this.sha = retrievedContent.sha;
|
||||
this.content = retrievedContent.content;
|
||||
this.url = retrievedContent.url;
|
||||
this.git_url = retrievedContent.git_url;
|
||||
this.html_url = retrievedContent.html_url;
|
||||
|
||||
return content;
|
||||
else
|
||||
return Base64.encodeBase64String(IOUtils.toByteArray(read()));
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
@@ -101,6 +109,21 @@ public class GHContent {
|
||||
return html_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actual content stored here.
|
||||
*/
|
||||
public InputStream read() throws IOException {
|
||||
return new Requester(root).asStream(getDownloadUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to retrieve the raw content of the file. Null if this is a directory.
|
||||
*/
|
||||
public String getDownloadUrl() throws IOException {
|
||||
populate();
|
||||
return download_url;
|
||||
}
|
||||
|
||||
public boolean isFile() {
|
||||
return "file".equals(type);
|
||||
}
|
||||
@@ -109,6 +132,16 @@ public class GHContent {
|
||||
return "dir".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (download_url!=null) return; // already populated
|
||||
root.retrieve().to(url, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* List immediate children of this directory.
|
||||
*/
|
||||
@@ -118,10 +151,10 @@ public class GHContent {
|
||||
|
||||
return new PagedIterable<GHContent>() {
|
||||
public PagedIterator<GHContent> iterator() {
|
||||
return new PagedIterator<GHContent>(owner.root.retrieve().asIterator(url, GHContent[].class)) {
|
||||
return new PagedIterator<GHContent>(root.retrieve().asIterator(url, GHContent[].class)) {
|
||||
@Override
|
||||
protected void wrapUp(GHContent[] page) {
|
||||
GHContent.wrap(page,owner);
|
||||
GHContent.wrap(page, repository);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -143,7 +176,7 @@ public class GHContent {
|
||||
public GHContentUpdateResponse update(byte[] newContentBytes, String commitMessage, String branch) throws IOException {
|
||||
String encodedContent = DatatypeConverter.printBase64Binary(newContentBytes);
|
||||
|
||||
Requester requester = new Requester(owner.root)
|
||||
Requester requester = new Requester(root)
|
||||
.with("path", path)
|
||||
.with("message", commitMessage)
|
||||
.with("sha", sha)
|
||||
@@ -156,8 +189,8 @@ public class GHContent {
|
||||
|
||||
GHContentUpdateResponse response = requester.to(getApiRoute(), GHContentUpdateResponse.class);
|
||||
|
||||
response.getContent().wrap(owner);
|
||||
response.getCommit().wrapUp(owner);
|
||||
response.getContent().wrap(repository);
|
||||
response.getCommit().wrapUp(repository);
|
||||
|
||||
this.content = encodedContent;
|
||||
return response;
|
||||
@@ -168,7 +201,7 @@ public class GHContent {
|
||||
}
|
||||
|
||||
public GHContentUpdateResponse delete(String commitMessage, String branch) throws IOException {
|
||||
Requester requester = new Requester(owner.root)
|
||||
Requester requester = new Requester(root)
|
||||
.with("path", path)
|
||||
.with("message", commitMessage)
|
||||
.with("sha", sha)
|
||||
@@ -180,18 +213,26 @@ public class GHContent {
|
||||
|
||||
GHContentUpdateResponse response = requester.to(getApiRoute(), GHContentUpdateResponse.class);
|
||||
|
||||
response.getCommit().wrapUp(owner);
|
||||
response.getCommit().wrapUp(repository);
|
||||
return response;
|
||||
}
|
||||
|
||||
private String getApiRoute() {
|
||||
return "/repos/" + owner.getOwnerName() + "/" + owner.getName() + "/contents/" + path;
|
||||
return "/repos/" + repository.getOwnerName() + "/" + repository.getName() + "/contents/" + path;
|
||||
}
|
||||
|
||||
GHContent wrap(GHRepository owner) {
|
||||
this.owner = owner;
|
||||
this.repository = owner;
|
||||
this.root = owner.root;
|
||||
return this;
|
||||
}
|
||||
GHContent wrap(GitHub root) {
|
||||
this.root = root;
|
||||
if (repository!=null)
|
||||
repository.wrap(root);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public static GHContent[] wrap(GHContent[] contents, GHRepository repository) {
|
||||
for (GHContent unwrappedContent : contents) {
|
||||
|
||||
74
src/main/java/org/kohsuke/github/GHContentSearchBuilder.java
Normal file
74
src/main/java/org/kohsuke/github/GHContentSearchBuilder.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
/**
|
||||
* Search code for {@link GHContent}.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see GitHub#searchContent()
|
||||
*/
|
||||
public class GHContentSearchBuilder extends GHSearchBuilder<GHContent> {
|
||||
/*package*/ GHContentSearchBuilder(GitHub root) {
|
||||
super(root,ContentSearchResult.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search terms.
|
||||
*/
|
||||
public GHContentSearchBuilder q(String term) {
|
||||
super.q(term);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GHContentSearchBuilder in(String v) {
|
||||
return q("in:"+v);
|
||||
}
|
||||
|
||||
public GHContentSearchBuilder language(String v) {
|
||||
return q("language:"+v);
|
||||
}
|
||||
|
||||
public GHContentSearchBuilder fork(String v) {
|
||||
return q("fork:"+v);
|
||||
}
|
||||
|
||||
public GHContentSearchBuilder size(String v) {
|
||||
return q("size:"+v);
|
||||
}
|
||||
|
||||
public GHContentSearchBuilder path(String v) {
|
||||
return q("path:"+v);
|
||||
}
|
||||
|
||||
public GHContentSearchBuilder filename(String v) {
|
||||
return q("filename:"+v);
|
||||
}
|
||||
|
||||
public GHContentSearchBuilder extension(String v) {
|
||||
return q("extension:"+v);
|
||||
}
|
||||
|
||||
public GHContentSearchBuilder user(String v) {
|
||||
return q("user:"+v);
|
||||
}
|
||||
|
||||
|
||||
public GHContentSearchBuilder repo(String v) {
|
||||
return q("repo:"+v);
|
||||
}
|
||||
|
||||
private static class ContentSearchResult extends SearchResult<GHContent> {
|
||||
private GHContent[] items;
|
||||
|
||||
@Override
|
||||
/*package*/ GHContent[] getItems(GitHub root) {
|
||||
for (GHContent item : items)
|
||||
item.wrap(root);
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getApiUrl() {
|
||||
return "/search/code";
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
@@ -12,21 +8,16 @@ import java.util.Locale;
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see GitHub#searchIssues()
|
||||
*/
|
||||
public class GHIssueSearchBuilder {
|
||||
private final GitHub root;
|
||||
private final Requester req;
|
||||
private final List<String> terms = new ArrayList<String>();
|
||||
|
||||
public class GHIssueSearchBuilder extends GHSearchBuilder<GHIssue> {
|
||||
/*package*/ GHIssueSearchBuilder(GitHub root) {
|
||||
this.root = root;
|
||||
req = root.retrieve();
|
||||
super(root,IssueSearchResult.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search terms.
|
||||
*/
|
||||
public GHIssueSearchBuilder q(String term) {
|
||||
terms.add(term);
|
||||
super.q(term);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -61,25 +52,15 @@ public class GHIssueSearchBuilder {
|
||||
private GHIssue[] items;
|
||||
|
||||
@Override
|
||||
public GHIssue[] getItems() {
|
||||
/*package*/ GHIssue[] getItems(GitHub root) {
|
||||
for (GHIssue i : items)
|
||||
i.wrap(root);
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists up the issues with the criteria built so far.
|
||||
*/
|
||||
public PagedSearchIterable<GHIssue> list() {
|
||||
return new PagedSearchIterable<GHIssue>() {
|
||||
public PagedIterator<GHIssue> iterator() {
|
||||
req.set("q", StringUtils.join(terms," "));
|
||||
return new PagedIterator<GHIssue>(adapt(req.asIterator("/search/issues", IssueSearchResult.class))) {
|
||||
protected void wrapUp(GHIssue[] page) {
|
||||
for (GHIssue c : page)
|
||||
c.wrap(root);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@Override
|
||||
protected String getApiUrl() {
|
||||
return "/search/issues";
|
||||
}
|
||||
}
|
||||
|
||||
207
src/main/java/org/kohsuke/github/GHNotificationStream.java
Normal file
207
src/main/java/org/kohsuke/github/GHNotificationStream.java
Normal file
@@ -0,0 +1,207 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Listens to GitHub notification stream.
|
||||
*
|
||||
* <p>
|
||||
* This class supports two modes of retrieving notifications that can
|
||||
* be controlled via {@link #nonBlocking(boolean)}.
|
||||
*
|
||||
* <p>
|
||||
* In the blocking mode, which is the default, iterator will be infinite.
|
||||
* The call to {@link Iterator#next()} will block until a new notification
|
||||
* arrives. This is useful for application that runs perpetually and reacts
|
||||
* to notifications.
|
||||
*
|
||||
* <p>
|
||||
* In the non-blocking mode, the iterator will only report the set of
|
||||
* notifications initially retrieved from GitHub, then quit. This is useful
|
||||
* for a batch application to process the current set of notifications.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see GitHub#listNotifications()
|
||||
* @see GHRepository#listNotifications()
|
||||
*/
|
||||
public class GHNotificationStream implements Iterable<GHThread> {
|
||||
private final GitHub root;
|
||||
|
||||
private Boolean all, participating;
|
||||
private String since;
|
||||
private String apiUrl;
|
||||
private boolean nonBlocking = false;
|
||||
|
||||
/*package*/ GHNotificationStream(GitHub root, String apiUrl) {
|
||||
this.root = root;
|
||||
this.apiUrl = apiUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the stream include notifications that are already read?
|
||||
*/
|
||||
public GHNotificationStream read(boolean v) {
|
||||
all = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the stream be restricted to notifications in which the user
|
||||
* is directly participating or mentioned?
|
||||
*/
|
||||
public GHNotificationStream participating(boolean v) {
|
||||
participating = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GHNotificationStream since(long timestamp) {
|
||||
return since(new Date(timestamp));
|
||||
}
|
||||
|
||||
public GHNotificationStream since(Date dt) {
|
||||
since = GitHub.printDate(dt);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to true, {@link #iterator()} will stop iterating instead of blocking and
|
||||
* waiting for the updates to arrive.
|
||||
*/
|
||||
public GHNotificationStream nonBlocking(boolean v) {
|
||||
this.nonBlocking = v;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an infinite blocking {@link Iterator} that returns
|
||||
* {@link GHThread} as notifications arrive.
|
||||
*/
|
||||
public Iterator<GHThread> iterator() {
|
||||
// capture the configuration setting here
|
||||
final Requester req = new Requester(root).method("GET")
|
||||
.with("all", all).with("participating", participating).with("since", since);
|
||||
|
||||
return new Iterator<GHThread>() {
|
||||
/**
|
||||
* Stuff we've fetched but haven't returned to the caller.
|
||||
* Newer ones first.
|
||||
*/
|
||||
private GHThread[] threads = EMPTY_ARRAY;
|
||||
|
||||
/**
|
||||
* Next element in {@link #threads} to return. This counts down.
|
||||
*/
|
||||
private int idx=-1;
|
||||
|
||||
/**
|
||||
* threads whose updated_at is older than this should be ignored.
|
||||
*/
|
||||
private long lastUpdated = -1;
|
||||
|
||||
/**
|
||||
* Next request should have "If-Modified-Since" header with this value.
|
||||
*/
|
||||
private String lastModified;
|
||||
|
||||
/**
|
||||
* When is the next polling allowed?
|
||||
*/
|
||||
private long nextCheckTime = -1;
|
||||
|
||||
private GHThread next;
|
||||
|
||||
public GHThread next() {
|
||||
if (next==null) {
|
||||
next = fetch();
|
||||
if (next==null)
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
GHThread r = next;
|
||||
next = null;
|
||||
return r;
|
||||
}
|
||||
|
||||
public boolean hasNext() {
|
||||
if (next==null)
|
||||
next = fetch();
|
||||
return next!=null;
|
||||
}
|
||||
|
||||
GHThread fetch() {
|
||||
try {
|
||||
while (true) {// loop until we get new threads to return
|
||||
|
||||
// if we have fetched un-returned threads, use them first
|
||||
while (idx>=0) {
|
||||
GHThread n = threads[idx--];
|
||||
long nt = n.getUpdatedAt().getTime();
|
||||
if (nt >= lastUpdated) {
|
||||
lastUpdated = nt;
|
||||
return n.wrap(root);
|
||||
}
|
||||
}
|
||||
|
||||
if (nonBlocking && nextCheckTime>=0)
|
||||
return null; // nothing more to report, and we aren't blocking
|
||||
|
||||
// observe the polling interval before making the call
|
||||
while (true) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (nextCheckTime < now) break;
|
||||
long waitTime = Math.max(Math.min(nextCheckTime - now, 1000), 60 * 1000);
|
||||
Thread.sleep(waitTime);
|
||||
}
|
||||
|
||||
req.setHeader("If-Modified-Since", lastModified);
|
||||
|
||||
threads = req.to(apiUrl, GHThread[].class);
|
||||
if (threads==null) {
|
||||
threads = EMPTY_ARRAY; // if unmodified, we get empty array
|
||||
} else {
|
||||
// we get a new batch, but we want to ignore the ones that we've seen
|
||||
lastUpdated++;
|
||||
}
|
||||
idx = threads.length-1;
|
||||
|
||||
nextCheckTime = calcNextCheckTime();
|
||||
lastModified = req.getResponseHeader("Last-Modified");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private long calcNextCheckTime() {
|
||||
String v = req.getResponseHeader("X-Poll-Interval");
|
||||
if (v==null) v="60";
|
||||
return System.currentTimeMillis()+Integer.parseInt(v)*1000;
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void markAsRead() throws IOException {
|
||||
markAsRead(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all the notifications as read.
|
||||
*/
|
||||
public void markAsRead(long timestamp) throws IOException {
|
||||
final Requester req = new Requester(root).method("PUT");
|
||||
if (timestamp>=0)
|
||||
req.with("last_read_at", GitHub.printDate(new Date(timestamp)));
|
||||
req.asHttpStatusCode(apiUrl);
|
||||
}
|
||||
|
||||
private static final GHThread[] EMPTY_ARRAY = new GHThread[0];
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Rate limit.
|
||||
* @author Kohsuke Kawaguchi
|
||||
@@ -10,12 +12,28 @@ public class GHRateLimit {
|
||||
*/
|
||||
public int remaining;
|
||||
/**
|
||||
* Alotted API call per hour.
|
||||
* Allotted API call per hour.
|
||||
*/
|
||||
public int limit;
|
||||
|
||||
/**
|
||||
* The time at which the current rate limit window resets in UTC epoch seconds.
|
||||
*/
|
||||
public Date reset;
|
||||
|
||||
/**
|
||||
* Non-epoch date
|
||||
*/
|
||||
public Date getResetDate() {
|
||||
return new Date(reset.getTime() * 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return remaining+"/"+limit;
|
||||
return "GHRateLimit{" +
|
||||
"remaining=" + remaining +
|
||||
", limit=" + limit +
|
||||
", resetDate=" + getResetDate() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ import org.apache.commons.lang.StringUtils;
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.Reader;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
@@ -285,6 +287,18 @@ public class GHRepository extends GHObject {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List languages for the specified repository.
|
||||
* The value on the right of a language is the number of bytes of code written in that language.
|
||||
* {
|
||||
"C": 78769,
|
||||
"Python": 7769
|
||||
}
|
||||
*/
|
||||
public Map<String,Long> listLanguages() throws IOException {
|
||||
return root.retrieve().to(getApiTailUrl("languages"), HashMap.class);
|
||||
}
|
||||
|
||||
public String getOwnerName() {
|
||||
return owner.login;
|
||||
}
|
||||
@@ -641,6 +655,35 @@ public class GHRepository extends GHObject {
|
||||
return root.retrieve().to(String.format("/repos/%s/%s/git/refs/%s", owner.login, name, refName), GHRef.class).wrap(root);
|
||||
}
|
||||
/**
|
||||
* Retrive a tree of the given type for the current GitHub repository.
|
||||
*
|
||||
* @param sha - sha number or branch name ex: "master"
|
||||
* @return refs matching the request type
|
||||
* @throws IOException
|
||||
* on failure communicating with GitHub, potentially due to an
|
||||
* invalid tree type being requested
|
||||
*/
|
||||
public GHTree getTree(String sha) throws IOException {
|
||||
String url = String.format("/repos/%s/%s/git/trees/%s", owner.login, name, sha);
|
||||
return root.retrieve().to(url, GHTree.class).wrap(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tree for the current GitHub repository, recursively as described in here:
|
||||
* https://developer.github.com/v3/git/trees/#get-a-tree-recursively
|
||||
*
|
||||
* @param sha - sha number or branch name ex: "master"
|
||||
* @param recursive use 1
|
||||
* @throws IOException
|
||||
* on failure communicating with GitHub, potentially due to an
|
||||
* invalid tree type being requested
|
||||
*/
|
||||
public GHTree getTreeRecursive(String sha, int recursive) throws IOException {
|
||||
String url = String.format("/repos/%s/%s/git/trees/%s?recursive=%d", owner.login, name, sha, recursive);
|
||||
return root.retrieve().to(url, GHTree.class).wrap(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a commit object in this repository.
|
||||
*/
|
||||
public GHCommit getCommit(String sha1) throws IOException {
|
||||
@@ -988,10 +1031,7 @@ public class GHRepository extends GHObject {
|
||||
Requester requester = root.retrieve();
|
||||
String target = getApiTailUrl("contents/" + path);
|
||||
|
||||
if (ref != null)
|
||||
target = target + "?ref=" + ref;
|
||||
|
||||
return requester.to(target, GHContent.class).wrap(this);
|
||||
return requester.with("ref",ref).to(target, GHContent.class).wrap(this);
|
||||
}
|
||||
|
||||
public List<GHContent> getDirectoryContent(String path) throws IOException {
|
||||
@@ -1002,10 +1042,7 @@ public class GHRepository extends GHObject {
|
||||
Requester requester = root.retrieve();
|
||||
String target = getApiTailUrl("contents/" + path);
|
||||
|
||||
if (ref != null)
|
||||
target = target + "?ref=" + ref;
|
||||
|
||||
GHContent[] files = requester.to(target, GHContent[].class);
|
||||
GHContent[] files = requester.with("ref",ref).to(target, GHContent[].class);
|
||||
|
||||
GHContent.wrap(files, this);
|
||||
|
||||
@@ -1114,8 +1151,32 @@ public class GHRepository extends GHObject {
|
||||
return contributions;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Render a Markdown document.
|
||||
*
|
||||
* In {@linkplain MarkdownMode#GFM GFM mode}, issue numbers and user mentions
|
||||
* are linked accordingly.
|
||||
*
|
||||
* @see GitHub#renderMarkdown(String)
|
||||
*/
|
||||
public Reader renderMarkdown(String text, MarkdownMode mode) throws IOException {
|
||||
return new InputStreamReader(
|
||||
new Requester(root)
|
||||
.with("text", text)
|
||||
.with("mode",mode==null?null:mode.toString())
|
||||
.with("context", getFullName())
|
||||
.asStream("/markdown"),
|
||||
"UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* List all the notifications in a repository for the current user.
|
||||
*/
|
||||
public GHNotificationStream listNotifications() {
|
||||
return new GHNotificationStream(root,getApiTailUrl("/notifications"));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Search repositories.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see GitHub#searchRepositories()
|
||||
*/
|
||||
public class GHRepositorySearchBuilder extends GHSearchBuilder<GHRepository> {
|
||||
/*package*/ GHRepositorySearchBuilder(GitHub root) {
|
||||
super(root,RepositorySearchResult.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search terms.
|
||||
*/
|
||||
public GHRepositorySearchBuilder q(String term) {
|
||||
super.q(term);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder in(String v) {
|
||||
return q("in:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder size(String v) {
|
||||
return q("size:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder forks(String v) {
|
||||
return q("forks:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder created(String v) {
|
||||
return q("created:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder pushed(String v) {
|
||||
return q("pushed:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder user(String v) {
|
||||
return q("user:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder repo(String v) {
|
||||
return q("repo:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder language(String v) {
|
||||
return q("language:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder stars(String v) {
|
||||
return q("stars:"+v);
|
||||
}
|
||||
|
||||
public GHRepositorySearchBuilder sort(Sort sort) {
|
||||
req.with("sort",sort.toString().toLowerCase(Locale.ENGLISH));
|
||||
return this;
|
||||
}
|
||||
|
||||
public enum Sort { STARS, FORKS, UPDATED }
|
||||
|
||||
private static class RepositorySearchResult extends SearchResult<GHRepository> {
|
||||
private GHRepository[] items;
|
||||
|
||||
@Override
|
||||
/*package*/ GHRepository[] getItems(GitHub root) {
|
||||
for (GHRepository item : items)
|
||||
item.wrap(root);
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getApiUrl() {
|
||||
return "/search/repositories";
|
||||
}
|
||||
}
|
||||
54
src/main/java/org/kohsuke/github/GHSearchBuilder.java
Normal file
54
src/main/java/org/kohsuke/github/GHSearchBuilder.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Base class for various search builders.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
public abstract class GHSearchBuilder<T> {
|
||||
protected final GitHub root;
|
||||
protected final Requester req;
|
||||
protected final List<String> terms = new ArrayList<String>();
|
||||
|
||||
/**
|
||||
* Data transfer object that receives the result of search.
|
||||
*/
|
||||
private final Class<? extends SearchResult<T>> receiverType;
|
||||
|
||||
/*package*/ GHSearchBuilder(GitHub root, Class<? extends SearchResult<T>> receiverType) {
|
||||
this.root = root;
|
||||
this.req = root.retrieve();
|
||||
this.receiverType = receiverType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search terms.
|
||||
*/
|
||||
public GHSearchBuilder q(String term) {
|
||||
terms.add(term);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the search.
|
||||
*/
|
||||
public PagedSearchIterable<T> list() {
|
||||
return new PagedSearchIterable<T>(root) {
|
||||
public PagedIterator<T> iterator() {
|
||||
req.set("q", StringUtils.join(terms, " "));
|
||||
return new PagedIterator<T>(adapt(req.asIterator(getApiUrl(), receiverType))) {
|
||||
protected void wrapUp(T[] page) {
|
||||
// SearchResult.getItems() should do it
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract String getApiUrl();
|
||||
}
|
||||
@@ -4,9 +4,11 @@ import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Represents your subscribing to a repository.
|
||||
* Represents your subscribing to a repository / conversation thread..
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see GHRepository#getSubscription()
|
||||
* @see GHThread#getSubscription()
|
||||
*/
|
||||
public class GHSubscription {
|
||||
private String created_at, url, repository_url, reason;
|
||||
|
||||
98
src/main/java/org/kohsuke/github/GHThread.java
Normal file
98
src/main/java/org/kohsuke/github/GHThread.java
Normal file
@@ -0,0 +1,98 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* A conversation in the notification API.
|
||||
*
|
||||
* @see <a href="https://developer.github.com/v3/activity/notifications/">documentation</a>
|
||||
* @see GHNotificationStream
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
public class GHThread extends GHObject {
|
||||
private GitHub root;
|
||||
private GHRepository repository;
|
||||
private Subject subject;
|
||||
private String reason;
|
||||
private boolean unread;
|
||||
private String last_read_at;
|
||||
private String url,subscription_url;
|
||||
|
||||
static class Subject {
|
||||
String title;
|
||||
String url;
|
||||
String latest_comment_url;
|
||||
String type;
|
||||
}
|
||||
|
||||
private GHThread() {// no external construction allowed
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if the entire thread has never been read.
|
||||
*/
|
||||
public Date getLastReadAt() {
|
||||
return GitHub.parseDate(last_read_at);
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public GHRepository getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
// TODO: how to expose the subject?
|
||||
|
||||
public boolean isRead() {
|
||||
return !unread;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return subject.title;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return subject.type;
|
||||
}
|
||||
|
||||
/*package*/ GHThread wrap(GitHub root) {
|
||||
this.root = root;
|
||||
if (this.repository!=null)
|
||||
this.repository.wrap(root);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this thread as read.
|
||||
*/
|
||||
public void markAsRead() throws IOException {
|
||||
new Requester(root).method("PATCH").to(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to this conversation to get notifications.
|
||||
*/
|
||||
public GHSubscription subscribe(boolean subscribed, boolean ignored) throws IOException {
|
||||
return new Requester(root)
|
||||
.with("subscribed", subscribed)
|
||||
.with("ignored", ignored)
|
||||
.method("PUT").to(subscription_url, GHSubscription.class).wrapUp(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current subscription for this thread.
|
||||
*
|
||||
* @return null if no subscription exists.
|
||||
*/
|
||||
public GHSubscription getSubscription() throws IOException {
|
||||
try {
|
||||
return new Requester(root).to(subscription_url, GHSubscription.class).wrapUp(root);
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/main/java/org/kohsuke/github/GHTree.java
Normal file
58
src/main/java/org/kohsuke/github/GHTree.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides information for Git Trees
|
||||
* https://developer.github.com/v3/git/trees/
|
||||
*
|
||||
* @author Daniel Teixeira - https://github.com/ddtxra
|
||||
* @see GHRepository#getTree(String)
|
||||
*/
|
||||
public class GHTree {
|
||||
/* package almost final */GitHub root;
|
||||
|
||||
private boolean truncated;
|
||||
private String sha, url;
|
||||
private GHTreeEntry[] tree;
|
||||
|
||||
/**
|
||||
* The SHA for this trees
|
||||
*/
|
||||
public String getSha() {
|
||||
return sha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of entries of the trees
|
||||
* @return
|
||||
*/
|
||||
public List<GHTreeEntry> getTree() {
|
||||
return Collections.unmodifiableList(Arrays.asList(tree));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the number of items in the tree array exceeded the GitHub maximum limit.
|
||||
* @return true true if the number of items in the tree array exceeded the GitHub maximum limit otherwise false.
|
||||
*/
|
||||
public boolean isTruncated() {
|
||||
return truncated;
|
||||
}
|
||||
|
||||
/**
|
||||
* The API URL of this tag, such as
|
||||
* "url": "https://api.github.com/repos/octocat/Hello-World/trees/fc6274d15fa3ae2ab983129fb037999f264ba9a7",
|
||||
*/
|
||||
public URL getUrl() {
|
||||
return GitHub.parseURL(url);
|
||||
}
|
||||
|
||||
/* package */GHTree wrap(GitHub root) {
|
||||
this.root = root;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
71
src/main/java/org/kohsuke/github/GHTreeEntry.java
Normal file
71
src/main/java/org/kohsuke/github/GHTreeEntry.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Provides information for Git Trees
|
||||
* https://developer.github.com/v3/git/trees/
|
||||
*
|
||||
* @author Daniel Teixeira - https://github.com/ddtxra
|
||||
* @see GHTree
|
||||
*/
|
||||
public class GHTreeEntry {
|
||||
private String path, mode, type, sha, url;
|
||||
private long size;
|
||||
|
||||
/**
|
||||
* Get the path such as
|
||||
* "subdir/file.txt"
|
||||
*
|
||||
* @return the path
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mode such as
|
||||
* 100644
|
||||
*
|
||||
* @return the mode
|
||||
*/
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of the file, such as
|
||||
* 132
|
||||
* @return The size of the path or 0 if it is a directory
|
||||
*/
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type such as:
|
||||
* "blob"
|
||||
*
|
||||
* @return The type
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* SHA1 of this object.
|
||||
*/
|
||||
public String getSha() {
|
||||
return sha;
|
||||
}
|
||||
|
||||
/**
|
||||
* API URL to this Git data, such as
|
||||
* https://api.github.com/repos/jenkinsci
|
||||
* /jenkins/git/commits/b72322675eb0114363a9a86e9ad5a170d1d07ac0
|
||||
*/
|
||||
public URL getUrl() {
|
||||
return GitHub.parseURL(url);
|
||||
}
|
||||
}
|
||||
72
src/main/java/org/kohsuke/github/GHUserSearchBuilder.java
Normal file
72
src/main/java/org/kohsuke/github/GHUserSearchBuilder.java
Normal file
@@ -0,0 +1,72 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Search users.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see GitHub#searchUsers()
|
||||
*/
|
||||
public class GHUserSearchBuilder extends GHSearchBuilder<GHUser> {
|
||||
/*package*/ GHUserSearchBuilder(GitHub root) {
|
||||
super(root,UserSearchResult.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search terms.
|
||||
*/
|
||||
public GHUserSearchBuilder q(String term) {
|
||||
super.q(term);
|
||||
return this;
|
||||
}
|
||||
|
||||
public GHUserSearchBuilder type(String v) {
|
||||
return q("type:"+v);
|
||||
}
|
||||
|
||||
public GHUserSearchBuilder in(String v) {
|
||||
return q("in:"+v);
|
||||
}
|
||||
|
||||
public GHUserSearchBuilder repos(String v) {
|
||||
return q("repos:"+v);
|
||||
}
|
||||
|
||||
public GHUserSearchBuilder location(String v) {
|
||||
return q("location:"+v);
|
||||
}
|
||||
|
||||
public GHUserSearchBuilder language(String v) {
|
||||
return q("language:"+v);
|
||||
}
|
||||
|
||||
public GHUserSearchBuilder created(String v) {
|
||||
return q("created:"+v);
|
||||
}
|
||||
|
||||
public GHUserSearchBuilder followers(String v) {
|
||||
return q("followers:"+v);
|
||||
}
|
||||
|
||||
public GHUserSearchBuilder sort(Sort sort) {
|
||||
req.with("sort",sort.toString().toLowerCase(Locale.ENGLISH));
|
||||
return this;
|
||||
}
|
||||
|
||||
public enum Sort { FOLLOWERS, REPOSITORIES, JOINED }
|
||||
|
||||
private static class UserSearchResult extends SearchResult<GHUser> {
|
||||
private GHUser[] items;
|
||||
|
||||
@Override
|
||||
/*package*/ GHUser[] getItems(GitHub root) {
|
||||
return GHUser.wrap(items,root);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getApiUrl() {
|
||||
return "/search/users";
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,10 @@ package org.kohsuke.github;
|
||||
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
|
||||
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
@@ -75,6 +77,8 @@ public class GitHub {
|
||||
|
||||
private final String apiUrl;
|
||||
|
||||
/*package*/ final RateLimitHandler rateLimitHandler;
|
||||
|
||||
private HttpConnector connector = HttpConnector.DEFAULT;
|
||||
|
||||
/**
|
||||
@@ -113,7 +117,7 @@ public class GitHub {
|
||||
* @param connector
|
||||
* HttpConnector to use. Pass null to use default connector.
|
||||
*/
|
||||
/* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector) throws IOException {
|
||||
/* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector, RateLimitHandler rateLimitHandler) throws IOException {
|
||||
if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize
|
||||
this.apiUrl = apiUrl;
|
||||
if (null != connector) this.connector = connector;
|
||||
@@ -132,6 +136,7 @@ public class GitHub {
|
||||
if (login==null && encodedAuthorization!=null)
|
||||
login = getMyself().getLogin();
|
||||
this.login = login;
|
||||
this.rateLimitHandler = rateLimitHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,7 +295,7 @@ public class GitHub {
|
||||
GHUser u = users.get(orig.getLogin());
|
||||
if (u==null) {
|
||||
orig.root = this;
|
||||
users.put(login,orig);
|
||||
users.put(orig.getLogin(),orig);
|
||||
return orig;
|
||||
}
|
||||
return u;
|
||||
@@ -331,27 +336,27 @@ public class GitHub {
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets complete map of organizations/teams that current user belongs to.
|
||||
*
|
||||
* Leverages the new GitHub API /user/teams made available recently to
|
||||
* get in a single call the complete set of organizations, teams and permissions
|
||||
* in a single call.
|
||||
*/
|
||||
public Map<String, Set<GHTeam>> getMyTeams() throws IOException {
|
||||
Map<String, Set<GHTeam>> allMyTeams = new HashMap<String, Set<GHTeam>>();
|
||||
for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) {
|
||||
team.wrapUp(this);
|
||||
String orgLogin = team.getOrganization().getLogin();
|
||||
Set<GHTeam> teamsPerOrg = allMyTeams.get(orgLogin);
|
||||
if (teamsPerOrg == null) {
|
||||
teamsPerOrg = new HashSet<GHTeam>();
|
||||
}
|
||||
teamsPerOrg.add(team);
|
||||
allMyTeams.put(orgLogin, teamsPerOrg);
|
||||
/**
|
||||
* Gets complete map of organizations/teams that current user belongs to.
|
||||
*
|
||||
* Leverages the new GitHub API /user/teams made available recently to
|
||||
* get in a single call the complete set of organizations, teams and permissions
|
||||
* in a single call.
|
||||
*/
|
||||
public Map<String, Set<GHTeam>> getMyTeams() throws IOException {
|
||||
Map<String, Set<GHTeam>> allMyTeams = new HashMap<String, Set<GHTeam>>();
|
||||
for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) {
|
||||
team.wrapUp(this);
|
||||
String orgLogin = team.getOrganization().getLogin();
|
||||
Set<GHTeam> teamsPerOrg = allMyTeams.get(orgLogin);
|
||||
if (teamsPerOrg == null) {
|
||||
teamsPerOrg = new HashSet<GHTeam>();
|
||||
}
|
||||
teamsPerOrg.add(team);
|
||||
allMyTeams.put(orgLogin, teamsPerOrg);
|
||||
}
|
||||
return allMyTeams;
|
||||
}
|
||||
return allMyTeams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public events visible to you. Equivalent of what's displayed on https://github.com/
|
||||
@@ -438,6 +443,81 @@ public class GitHub {
|
||||
return new GHIssueSearchBuilder(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search users.
|
||||
*/
|
||||
public GHUserSearchBuilder searchUsers() {
|
||||
return new GHUserSearchBuilder(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search repositories.
|
||||
*/
|
||||
public GHRepositorySearchBuilder searchRepositories() {
|
||||
return new GHRepositorySearchBuilder(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search content.
|
||||
*/
|
||||
public GHContentSearchBuilder searchContent() {
|
||||
return new GHContentSearchBuilder(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all the notifications.
|
||||
*/
|
||||
public GHNotificationStream listNotifications() {
|
||||
return new GHNotificationStream(this,"/notifications");
|
||||
}
|
||||
|
||||
/**
|
||||
* This provides a dump of every public repository, in the order that they were created.
|
||||
* @see <a href="https://developer.github.com/v3/repos/#list-all-public-repositories">documentation</a>
|
||||
*/
|
||||
public PagedIterable<GHRepository> listAllPublicRepositories() {
|
||||
return listAllPublicRepositories(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This provides a dump of every public repository, in the order that they were created.
|
||||
*
|
||||
* @param since
|
||||
* The integer ID of the last Repository that you’ve seen. See {@link GHRepository#getId()}
|
||||
* @see <a href="https://developer.github.com/v3/repos/#list-all-public-repositories">documentation</a>
|
||||
*/
|
||||
public PagedIterable<GHRepository> listAllPublicRepositories(final String since) {
|
||||
return new PagedIterable<GHRepository>() {
|
||||
public PagedIterator<GHRepository> iterator() {
|
||||
return new PagedIterator<GHRepository>(retrieve().with("since",since).asIterator("/repositories", GHRepository[].class)) {
|
||||
@Override
|
||||
protected void wrapUp(GHRepository[] page) {
|
||||
for (GHRepository c : page)
|
||||
c.wrap(GitHub.this);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Markdown document in raw mode.
|
||||
*
|
||||
* <p>
|
||||
* It takes a Markdown document as plaintext and renders it as plain Markdown
|
||||
* without a repository context (just like a README.md file is rendered – this
|
||||
* is the simplest way to preview a readme online).
|
||||
*
|
||||
* @see GHRepository#renderMarkdown(String, MarkdownMode)
|
||||
*/
|
||||
public Reader renderMarkdown(String text) throws IOException {
|
||||
return new InputStreamReader(
|
||||
new Requester(this)
|
||||
.with(new ByteArrayInputStream(text.getBytes("UTF-8")))
|
||||
.contentType("text/plain;charset=UTF-8")
|
||||
.asStream("/markdown/raw"),
|
||||
"UTF-8");
|
||||
}
|
||||
|
||||
/*package*/ static URL parseURL(String s) {
|
||||
try {
|
||||
|
||||
@@ -9,102 +9,119 @@ import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @since 1.59
|
||||
*/
|
||||
public class GitHubBuilder {
|
||||
private String endpoint = GitHub.GITHUB_URL;
|
||||
|
||||
|
||||
// default scoped so unit tests can read them.
|
||||
/* private */ String endpoint = GitHub.GITHUB_URL;
|
||||
/* private */ String user;
|
||||
/* private */ String password;
|
||||
/* private */ String oauthToken;
|
||||
|
||||
private HttpConnector connector;
|
||||
|
||||
private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
|
||||
|
||||
public GitHubBuilder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* First check if the credentials are configured using the ~/.github properties file.
|
||||
*
|
||||
*
|
||||
* If no user is specified it means there is no configuration present so check the environment instead.
|
||||
*
|
||||
*
|
||||
* If there is still no user it means there are no credentials defined and throw an IOException.
|
||||
*
|
||||
*
|
||||
* @return the configured Builder from credentials defined on the system or in the environment.
|
||||
*
|
||||
*
|
||||
* @throws IOException If there are no credentials defined in the ~/.github properties file or the process environment.
|
||||
*/
|
||||
public static GitHubBuilder fromCredentials() throws IOException {
|
||||
|
||||
Exception cause = null;
|
||||
GitHubBuilder builder;
|
||||
|
||||
try {
|
||||
builder = fromPropertyFile();
|
||||
|
||||
|
||||
if (builder.user != null)
|
||||
return builder;
|
||||
else {
|
||||
|
||||
// this is the case where the ~/.github file exists but has no content.
|
||||
|
||||
builder = fromEnvironment();
|
||||
|
||||
if (builder.user != null)
|
||||
return builder;
|
||||
else
|
||||
throw new IOException("Failed to resolve credentials from ~/.github or the environment.");
|
||||
}
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
builder = fromEnvironment();
|
||||
|
||||
if (builder.user != null)
|
||||
return builder;
|
||||
else
|
||||
throw new IOException("Failed to resolve credentials from ~/.github or the environment.", e);
|
||||
// fall through
|
||||
cause = e;
|
||||
}
|
||||
|
||||
|
||||
builder = fromEnvironment();
|
||||
|
||||
if (builder.user != null)
|
||||
return builder;
|
||||
else
|
||||
throw (IOException)new IOException("Failed to resolve credentials from ~/.github or the environment.").initCause(cause);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use {@link #fromEnvironment()} to pick up standard set of environment variables, so that
|
||||
* different clients of this library will all recognize one consistent set of coordinates.
|
||||
*/
|
||||
public static GitHubBuilder fromEnvironment(String loginVariableName, String passwordVariableName, String oauthVariableName) throws IOException {
|
||||
|
||||
|
||||
Properties env = new Properties();
|
||||
|
||||
Object loginValue = System.getenv(loginVariableName);
|
||||
|
||||
if (loginValue != null)
|
||||
env.put("login", loginValue);
|
||||
|
||||
Object passwordValue = System.getenv(passwordVariableName);
|
||||
|
||||
if (passwordValue != null)
|
||||
env.put("password", passwordValue);
|
||||
|
||||
Object oauthValue = System.getenv(oauthVariableName);
|
||||
|
||||
if (oauthValue != null)
|
||||
env.put("oauth", oauthValue);
|
||||
|
||||
return fromProperties(env);
|
||||
|
||||
return fromEnvironment(loginVariableName, passwordVariableName, oauthVariableName, "");
|
||||
}
|
||||
|
||||
|
||||
private static void loadIfSet(String envName, Properties p, String propName) {
|
||||
String v = System.getenv(envName);
|
||||
if (v != null)
|
||||
p.put(propName, v);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use {@link #fromEnvironment()} to pick up standard set of environment variables, so that
|
||||
* different clients of this library will all recognize one consistent set of coordinates.
|
||||
*/
|
||||
public static GitHubBuilder fromEnvironment(String loginVariableName, String passwordVariableName, String oauthVariableName, String endpointVariableName) throws IOException {
|
||||
Properties env = new Properties();
|
||||
loadIfSet(loginVariableName,env,"login");
|
||||
loadIfSet(passwordVariableName,env,"password");
|
||||
loadIfSet(oauthVariableName,env,"oauth");
|
||||
loadIfSet(endpointVariableName,env,"endpoint");
|
||||
return fromProperties(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link GitHubBuilder} by picking up coordinates from environment variables.
|
||||
*
|
||||
* <p>
|
||||
* The following environment variables are recognized:
|
||||
*
|
||||
* <ul>
|
||||
* <li>GITHUB_LOGIN: username like 'kohsuke'
|
||||
* <li>GITHUB_PASSWORD: raw password
|
||||
* <li>GITHUB_OAUTH: OAuth token to login
|
||||
* <li>GITHUB_ENDPOINT: URL of the API endpoint
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* See class javadoc for the relationship between these coordinates.
|
||||
*
|
||||
* <p>
|
||||
* For backward compatibility, the following environment variables are recognized but discouraged:
|
||||
* login, password, oauth
|
||||
*/
|
||||
public static GitHubBuilder fromEnvironment() throws IOException {
|
||||
|
||||
Properties props = new Properties();
|
||||
|
||||
Map<String, String> env = System.getenv();
|
||||
|
||||
for (Map.Entry<String, String> element : env.entrySet()) {
|
||||
|
||||
props.put(element.getKey(), element.getValue());
|
||||
}
|
||||
|
||||
for (Entry<String, String> e : System.getenv().entrySet()) {
|
||||
String name = e.getKey().toLowerCase(Locale.ENGLISH);
|
||||
if (name.startsWith("github_")) name=name.substring(7);
|
||||
props.put(name,e.getValue());
|
||||
}
|
||||
return fromProperties(props);
|
||||
}
|
||||
|
||||
@@ -113,7 +130,7 @@ public class GitHubBuilder {
|
||||
File propertyFile = new File(homeDir, ".github");
|
||||
return fromPropertyFile(propertyFile.getPath());
|
||||
}
|
||||
|
||||
|
||||
public static GitHubBuilder fromPropertyFile(String propertyFileName) throws IOException {
|
||||
Properties props = new Properties();
|
||||
FileInputStream in = null;
|
||||
@@ -131,6 +148,7 @@ public class GitHubBuilder {
|
||||
GitHubBuilder self = new GitHubBuilder();
|
||||
self.withOAuthToken(props.getProperty("oauth"), props.getProperty("login"));
|
||||
self.withPassword(props.getProperty("login"), props.getProperty("password"));
|
||||
self.withEndpoint(props.getProperty("endpoint", GitHub.GITHUB_URL));
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -155,6 +173,10 @@ public class GitHubBuilder {
|
||||
this.connector = connector;
|
||||
return this;
|
||||
}
|
||||
public GitHubBuilder withRateLimitHandler(RateLimitHandler handler) {
|
||||
this.rateLimitHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures {@linkplain #withConnector(HttpConnector) connector}
|
||||
@@ -170,6 +192,6 @@ public class GitHubBuilder {
|
||||
}
|
||||
|
||||
public GitHub build() throws IOException {
|
||||
return new GitHub(endpoint, user, oauthToken, password, connector);
|
||||
return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler);
|
||||
}
|
||||
}
|
||||
|
||||
29
src/main/java/org/kohsuke/github/MarkdownMode.java
Normal file
29
src/main/java/org/kohsuke/github/MarkdownMode.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Rendering mode of markdown.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see GitHub#renderMarkdown(String)
|
||||
* @see GHRepository#renderMarkdown(String, MarkdownMode)
|
||||
*/
|
||||
public enum MarkdownMode {
|
||||
/**
|
||||
* Render a document as plain Markdown, just like README files are rendered.
|
||||
*/
|
||||
MARKDOWN,
|
||||
/**
|
||||
* Render a document as user-content, e.g. like user comments or issues are rendered.
|
||||
* In GFM mode, hard line breaks are always taken into account, and issue and user
|
||||
* mentions are linked accordingly.
|
||||
*
|
||||
* @see GHRepository#renderMarkdown(String, MarkdownMode)
|
||||
*/
|
||||
GFM;
|
||||
|
||||
public String toString() {
|
||||
return name().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,17 @@ import java.util.Iterator;
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
public abstract class PagedSearchIterable<T> extends PagedIterable<T> {
|
||||
private final GitHub root;
|
||||
|
||||
/**
|
||||
* As soon as we have any result fetched, it's set here so that we can report the total count.
|
||||
*/
|
||||
private SearchResult<T> result;
|
||||
|
||||
/*package*/ PagedSearchIterable(GitHub root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of hit, including the results that's not yet fetched.
|
||||
*/
|
||||
@@ -43,7 +49,7 @@ public abstract class PagedSearchIterable<T> extends PagedIterable<T> {
|
||||
public T[] next() {
|
||||
SearchResult<T> v = base.next();
|
||||
if (result==null) result = v;
|
||||
return v.getItems();
|
||||
return v.getItems(root);
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
|
||||
61
src/main/java/org/kohsuke/github/RateLimitHandler.java
Normal file
61
src/main/java/org/kohsuke/github/RateLimitHandler.java
Normal file
@@ -0,0 +1,61 @@
|
||||
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 rate limit is reached.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @see GitHubBuilder#withRateLimitHandler(RateLimitHandler)
|
||||
*/
|
||||
public abstract class RateLimitHandler {
|
||||
/**
|
||||
* Called when the library encounters HTTP error indicating that the API rate 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/#rate-limiting">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;
|
||||
|
||||
/**
|
||||
* Block until the API rate limit is reset. Useful for long-running batch processing.
|
||||
*/
|
||||
public static final RateLimitHandler WAIT = new RateLimitHandler() {
|
||||
@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("X-RateLimit-Reset");
|
||||
if (v==null) return 10000; // can't tell
|
||||
|
||||
return Math.max(10000, Long.parseLong(v)*1000 - System.currentTimeMillis());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fail immediately.
|
||||
*/
|
||||
public static final RateLimitHandler FAIL = new RateLimitHandler() {
|
||||
@Override
|
||||
public void onError(IOException e, HttpURLConnection uc) throws IOException {
|
||||
throw (IOException)new IOException("API rate limit reached").initCause(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -23,13 +23,13 @@
|
||||
*/
|
||||
package org.kohsuke.github;
|
||||
|
||||
import static org.kohsuke.github.GitHub.MAPPER;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.Reader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Array;
|
||||
@@ -44,6 +44,7 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
@@ -52,9 +53,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import static org.kohsuke.github.GitHub.*;
|
||||
|
||||
/**
|
||||
* A builder pattern for making HTTP call and parsing its output.
|
||||
@@ -64,6 +63,7 @@ import javax.net.ssl.HttpsURLConnection;
|
||||
class Requester {
|
||||
private final GitHub root;
|
||||
private final List<Entry> args = new ArrayList<Entry>();
|
||||
private final Map<String,String> headers = new LinkedHashMap<String, String>();
|
||||
|
||||
/**
|
||||
* Request method.
|
||||
@@ -72,6 +72,11 @@ class Requester {
|
||||
private String contentType = "application/x-www-form-urlencoded";
|
||||
private InputStream body;
|
||||
|
||||
/**
|
||||
* Current connection.
|
||||
*/
|
||||
private HttpURLConnection uc;
|
||||
|
||||
private static class Entry {
|
||||
String key;
|
||||
Object value;
|
||||
@@ -86,6 +91,15 @@ class Requester {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request HTTP header.
|
||||
*
|
||||
* If a header of the same name is already set, this method overrides it.
|
||||
*/
|
||||
public void setHeader(String name, String value) {
|
||||
headers.put(name,value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a request with authentication credential.
|
||||
*/
|
||||
@@ -109,6 +123,10 @@ class Requester {
|
||||
public Requester with(String key, boolean value) {
|
||||
return _with(key, value);
|
||||
}
|
||||
public Requester with(String key, Boolean value) {
|
||||
return _with(key, value);
|
||||
}
|
||||
|
||||
|
||||
public Requester with(String key, String value) {
|
||||
return _with(key, value);
|
||||
@@ -190,12 +208,20 @@ class Requester {
|
||||
|
||||
private <T> T _to(String tailApiUrl, Class<T> type, T instance) throws IOException {
|
||||
while (true) {// loop while API rate limit is hit
|
||||
HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl));
|
||||
if (method.equals("GET") && !args.isEmpty()) {
|
||||
StringBuilder qs=new StringBuilder();
|
||||
for (Entry arg : args) {
|
||||
qs.append(qs.length()==0 ? '?' : '&');
|
||||
qs.append(arg.key).append('=').append(URLEncoder.encode(arg.value.toString(),"UTF-8"));
|
||||
}
|
||||
tailApiUrl += qs.toString();
|
||||
}
|
||||
setupConnection(root.getApiURL(tailApiUrl));
|
||||
|
||||
buildRequest(uc);
|
||||
buildRequest();
|
||||
|
||||
try {
|
||||
T result = parse(uc, type, instance);
|
||||
T result = parse(type, instance);
|
||||
if (type != null && type.isArray()) { // we might have to loop for pagination - done through recursion
|
||||
final String links = uc.getHeaderField("link");
|
||||
if (links != null && links.contains("rel=\"next\"")) {
|
||||
@@ -216,7 +242,7 @@ class Requester {
|
||||
}
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
handleApiError(e,uc);
|
||||
handleApiError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,19 +252,41 @@ class Requester {
|
||||
*/
|
||||
public int asHttpStatusCode(String tailApiUrl) throws IOException {
|
||||
while (true) {// loop while API rate limit is hit
|
||||
HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl));
|
||||
setupConnection(root.getApiURL(tailApiUrl));
|
||||
|
||||
buildRequest(uc);
|
||||
buildRequest();
|
||||
|
||||
try {
|
||||
return uc.getResponseCode();
|
||||
} catch (IOException e) {
|
||||
handleApiError(e,uc);
|
||||
handleApiError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void buildRequest(HttpURLConnection uc) throws IOException {
|
||||
public InputStream asStream(String tailApiUrl) throws IOException {
|
||||
while (true) {// loop while API rate limit is hit
|
||||
setupConnection(root.getApiURL(tailApiUrl));
|
||||
|
||||
buildRequest();
|
||||
|
||||
try {
|
||||
return wrapStream(uc.getInputStream());
|
||||
} catch (IOException e) {
|
||||
handleApiError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getResponseHeader(String header) {
|
||||
return uc.getHeaderField(header);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set up the request parameters or POST payload.
|
||||
*/
|
||||
private void buildRequest() throws IOException {
|
||||
if (!method.equals("GET")) {
|
||||
uc.setDoOutput(true);
|
||||
uc.setRequestProperty("Content-type", contentType);
|
||||
@@ -327,14 +375,14 @@ class Requester {
|
||||
|
||||
try {
|
||||
while (true) {// loop while API rate limit is hit
|
||||
HttpURLConnection uc = setupConnection(url);
|
||||
setupConnection(url);
|
||||
try {
|
||||
next = parse(uc,type,null);
|
||||
next = parse(type,null);
|
||||
assert next!=null;
|
||||
findNextURL(uc);
|
||||
findNextURL();
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
handleApiError(e,uc);
|
||||
handleApiError(e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -345,7 +393,7 @@ class Requester {
|
||||
/**
|
||||
* Locate the next page from the pagination "Link" tag.
|
||||
*/
|
||||
private void findNextURL(HttpURLConnection uc) throws MalformedURLException {
|
||||
private void findNextURL() throws MalformedURLException {
|
||||
url = null; // start defensively
|
||||
String link = uc.getHeaderField("Link");
|
||||
if (link==null) return;
|
||||
@@ -366,14 +414,20 @@ class Requester {
|
||||
}
|
||||
|
||||
|
||||
private HttpURLConnection setupConnection(URL url) throws IOException {
|
||||
HttpURLConnection uc = root.getConnector().connect(url);
|
||||
private void setupConnection(URL url) throws IOException {
|
||||
uc = root.getConnector().connect(url);
|
||||
|
||||
// if the authentication is needed but no credential is given, try it anyway (so that some calls
|
||||
// that do work with anonymous access in the reduced form should still work.)
|
||||
if (root.encodedAuthorization!=null)
|
||||
uc.setRequestProperty("Authorization", root.encodedAuthorization);
|
||||
|
||||
for (Map.Entry<String, String> e : headers.entrySet()) {
|
||||
String v = e.getValue();
|
||||
if (v!=null)
|
||||
uc.setRequestProperty(e.getKey(), v);
|
||||
}
|
||||
|
||||
try {
|
||||
uc.setRequestMethod(method);
|
||||
} catch (ProtocolException e) {
|
||||
@@ -387,16 +441,21 @@ class Requester {
|
||||
}
|
||||
}
|
||||
uc.setRequestProperty("Accept-Encoding", "gzip");
|
||||
return uc;
|
||||
}
|
||||
|
||||
private <T> T parse(HttpURLConnection uc, 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;
|
||||
try {
|
||||
r = new InputStreamReader(wrapStream(uc, uc.getInputStream()), "UTF-8");
|
||||
r = new InputStreamReader(wrapStream(uc.getInputStream()), "UTF-8");
|
||||
String data = IOUtils.toString(r);
|
||||
if (type!=null)
|
||||
return MAPPER.readValue(data,type);
|
||||
try {
|
||||
return MAPPER.readValue(data,type);
|
||||
} catch (JsonMappingException e) {
|
||||
throw (IOException)new IOException("Failed to deserialize "+data).initCause(e);
|
||||
}
|
||||
if (instance!=null)
|
||||
return MAPPER.readerForUpdating(instance).<T>readValue(data);
|
||||
return null;
|
||||
@@ -408,7 +467,7 @@ class Requester {
|
||||
/**
|
||||
* Handles the "Content-Encoding" header.
|
||||
*/
|
||||
private InputStream wrapStream(HttpURLConnection uc, InputStream in) throws IOException {
|
||||
private InputStream wrapStream(InputStream in) throws IOException {
|
||||
String encoding = uc.getContentEncoding();
|
||||
if (encoding==null || in==null) return in;
|
||||
if (encoding.equals("gzip")) return new GZIPInputStream(in);
|
||||
@@ -417,28 +476,22 @@ class Requester {
|
||||
}
|
||||
|
||||
/**
|
||||
* If the error is because of the API limit, wait 10 sec and return normally.
|
||||
* Otherwise throw an exception reporting an error.
|
||||
* Handle API error by either throwing it or by returning normally to retry.
|
||||
*/
|
||||
/*package*/ void handleApiError(IOException e, HttpURLConnection uc) throws IOException {
|
||||
/*package*/ void handleApiError(IOException e) throws IOException {
|
||||
if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) {
|
||||
// API limit reached. wait 10 secs and return normally
|
||||
try {
|
||||
Thread.sleep(10000);
|
||||
return;
|
||||
} catch (InterruptedException _) {
|
||||
throw (InterruptedIOException)new InterruptedIOException().initCause(e);
|
||||
}
|
||||
root.rateLimitHandler.onError(e,uc);
|
||||
}
|
||||
|
||||
if (e instanceof FileNotFoundException)
|
||||
throw e; // pass through 404 Not Found to allow the caller to handle it intelligently
|
||||
|
||||
InputStream es = wrapStream(uc, uc.getErrorStream());
|
||||
InputStream es = wrapStream(uc.getErrorStream());
|
||||
try {
|
||||
if (es!=null)
|
||||
throw (IOException)new IOException(IOUtils.toString(es,"UTF-8")).initCause(e);
|
||||
else
|
||||
if (es!=null) {
|
||||
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);
|
||||
} else
|
||||
throw e;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(es);
|
||||
|
||||
@@ -9,5 +9,8 @@ abstract class SearchResult<T> {
|
||||
int total_count;
|
||||
boolean incomplete_results;
|
||||
|
||||
public abstract T[] getItems();
|
||||
/**
|
||||
* Wraps up the retrieved object and return them. Only called once.
|
||||
*/
|
||||
/*package*/ abstract T[] getItems(GitHub root);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ are used in favor of using string handle (such as `GHUser.isMemberOf(GHOrganizat
|
||||
|
||||
The library supports both github.com and GitHub Enterprise.
|
||||
|
||||
There are some corners of the GitHub API that's not yet implemented, but
|
||||
the library is implemented with the right abstractions and libraries to make it very easy to improve the coverage.
|
||||
Most of the GitHub APIs are covered, although there are some corners that are still not yet implemented.
|
||||
|
||||
Sample Usage
|
||||
-----
|
||||
@@ -42,4 +41,11 @@ This library comes with a pluggable connector to use different HTTP client imple
|
||||
through `HttpConnector`. In particular, this means you can use [OkHttp](http://square.github.io/okhttp/),
|
||||
so we can make use of it's HTTP response cache.
|
||||
Making a conditional request against the GitHub API and receiving a 304 response
|
||||
[does not count against the rate limit](http://developer.github.com/v3/#conditional-requests).
|
||||
[does not count against the rate limit](http://developer.github.com/v3/#conditional-requests).
|
||||
|
||||
The following code shows an example of how to set up persistent cache on the disk:
|
||||
|
||||
Cache cache = new Cache(cacheDirectory, 10 * 1024 * 1024); // 10MB cache
|
||||
GitHub gitHub = GitHubBuilder.fromCredentials()
|
||||
.withConnector(new OkHttpConnector(new OkUrlFactory(new OkHttpClient().setCache(cache))))
|
||||
.build();
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.kohsuke.randname.RandomNameGenerator;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.util.Properties;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* @author Kohsuke Kawaguchi
|
||||
@@ -17,16 +15,11 @@ public abstract class AbstractGitHubApiTestBase extends Assert {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
Properties props = new Properties();
|
||||
java.io.File f = new java.io.File(System.getProperty("user.home"), ".github.kohsuke2");
|
||||
File f = new File(System.getProperty("user.home"), ".github.kohsuke2");
|
||||
if (f.exists()) {
|
||||
FileInputStream in = new FileInputStream(f);
|
||||
try {
|
||||
props.load(in);
|
||||
gitHub = GitHub.connect(props.getProperty("login"),props.getProperty("oauth"));
|
||||
} finally {
|
||||
IOUtils.closeQuietly(in);
|
||||
}
|
||||
// use the non-standard credential preferentially, so that developers of this library do not have
|
||||
// to clutter their event stream.
|
||||
gitHub = GitHubBuilder.fromPropertyFile(f.getPath()).build();
|
||||
} else {
|
||||
gitHub = GitHub.connect();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.kohsuke.github;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Test;
|
||||
import org.kohsuke.github.GHCommit.File;
|
||||
@@ -205,31 +207,31 @@ public class AppTest extends AbstractGitHubApiTestBase {
|
||||
|
||||
@Test
|
||||
public void testMyTeamsContainsAllMyOrganizations() throws IOException {
|
||||
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
|
||||
Map<String, GHOrganization> myOrganizations = gitHub.getMyOrganizations();
|
||||
assertEquals(teams.keySet(), myOrganizations.keySet());
|
||||
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
|
||||
Map<String, GHOrganization> myOrganizations = gitHub.getMyOrganizations();
|
||||
assertEquals(teams.keySet(), myOrganizations.keySet());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMyTeamsShouldIncludeMyself() throws IOException {
|
||||
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
|
||||
for (Entry<String, Set<GHTeam>> teamsPerOrg : teams.entrySet()) {
|
||||
String organizationName = teamsPerOrg.getKey();
|
||||
for (GHTeam team : teamsPerOrg.getValue()) {
|
||||
String teamName = team.getName();
|
||||
assertTrue("Team " + teamName + " in organization " + organizationName
|
||||
+ " does not contain myself",
|
||||
shouldBelongToTeam(organizationName, teamName));
|
||||
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
|
||||
for (Entry<String, Set<GHTeam>> teamsPerOrg : teams.entrySet()) {
|
||||
String organizationName = teamsPerOrg.getKey();
|
||||
for (GHTeam team : teamsPerOrg.getValue()) {
|
||||
String teamName = team.getName();
|
||||
assertTrue("Team " + teamName + " in organization " + organizationName
|
||||
+ " does not contain myself",
|
||||
shouldBelongToTeam(organizationName, teamName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldBelongToTeam(String organizationName, String teamName) throws IOException {
|
||||
GHOrganization org = gitHub.getOrganization(organizationName);
|
||||
assertNotNull(org);
|
||||
GHTeam team = org.getTeamByName(teamName);
|
||||
assertNotNull(team);
|
||||
return team.hasMember(gitHub.getMyself());
|
||||
GHOrganization org = gitHub.getOrganization(organizationName);
|
||||
assertNotNull(org);
|
||||
GHTeam team = org.getTeamByName(teamName);
|
||||
assertNotNull(team);
|
||||
return team.hasMember(gitHub.getMyself());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -665,6 +667,33 @@ public class AppTest extends AbstractGitHubApiTestBase {
|
||||
assertEquals(readme.getName(),"README.md");
|
||||
assertEquals(readme.getContent(),"This is a markdown readme.\n");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testTrees() throws IOException {
|
||||
GHTree masterTree = gitHub.getRepository("kohsuke/github-api").getTree("master");
|
||||
boolean foundReadme = false;
|
||||
for(GHTreeEntry e : masterTree.getTree()){
|
||||
if("readme".equalsIgnoreCase(e.getPath().replaceAll(".md", ""))){
|
||||
foundReadme = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(foundReadme);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTreesRecursive() throws IOException {
|
||||
GHTree masterTree = gitHub.getRepository("kohsuke/github-api").getTreeRecursive("master", 1);
|
||||
boolean foundThisFile = false;
|
||||
for(GHTreeEntry e : masterTree.getTree()){
|
||||
if(e.getPath().endsWith(AppTest.class.getSimpleName() + ".java")){
|
||||
foundThisFile = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(foundThisFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepoLabel() throws IOException {
|
||||
@@ -709,6 +738,95 @@ public class AppTest extends AbstractGitHubApiTestBase {
|
||||
assertTrue(githubApi);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListAllRepositories() throws Exception {
|
||||
Iterator<GHRepository> itr = gitHub.listAllPublicRepositories().iterator();
|
||||
for (int i=0; i<30; i++) {
|
||||
assertTrue(itr.hasNext());
|
||||
GHRepository r = itr.next();
|
||||
System.out.println(r.getFullName());
|
||||
assertNotNull(r.getUrl());
|
||||
assertNotEquals(0,r.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Test // issue #162
|
||||
public void testIssue162() throws Exception {
|
||||
GHRepository r = gitHub.getRepository("kohsuke/github-api");
|
||||
List<GHContent> contents = r.getDirectoryContent("", "gh-pages");
|
||||
for (GHContent content : contents) {
|
||||
if (content.isFile()) {
|
||||
String content1 = content.getContent();
|
||||
String content2 = r.getFileContent(content.getPath(), "gh-pages").getContent();
|
||||
System.out.println(content.getPath());
|
||||
assertEquals(content1, content2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void markDown() throws Exception {
|
||||
assertEquals("<p><strong>Test日本語</strong></p>", IOUtils.toString(gitHub.renderMarkdown("**Test日本語**")).trim());
|
||||
|
||||
String actual = IOUtils.toString(gitHub.getRepository("kohsuke/github-api").renderMarkdown("@kohsuke to fix issue #1", MarkdownMode.GFM));
|
||||
System.out.println(actual);
|
||||
assertTrue(actual.contains("href=\"https://github.com/kohsuke\""));
|
||||
assertTrue(actual.contains("href=\"https://github.com/kohsuke/github-api/pull/1\""));
|
||||
assertTrue(actual.contains("class=\"user-mention\""));
|
||||
assertTrue(actual.contains("class=\"issue-link\""));
|
||||
assertTrue(actual.contains("to fix issue"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchUsers() throws Exception {
|
||||
PagedSearchIterable<GHUser> r = gitHub.searchUsers().q("tom").repos(">42").followers(">1000").list();
|
||||
GHUser u = r.iterator().next();
|
||||
System.out.println(u.getName());
|
||||
assertNotNull(u.getId());
|
||||
assertTrue(r.getTotalCount() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchRepositories() throws Exception {
|
||||
PagedSearchIterable<GHRepository> r = gitHub.searchRepositories().q("tetris").language("assembly").sort(GHRepositorySearchBuilder.Sort.STARS).list();
|
||||
GHRepository u = r.iterator().next();
|
||||
System.out.println(u.getName());
|
||||
assertNotNull(u.getId());
|
||||
assertEquals("Assembly", u.getLanguage());
|
||||
assertTrue(r.getTotalCount() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void searchContent() throws Exception {
|
||||
PagedSearchIterable<GHContent> r = gitHub.searchContent().q("addClass").in("file").language("js").repo("jquery/jquery").list();
|
||||
GHContent c = r.iterator().next();
|
||||
System.out.println(c.getName());
|
||||
assertNotNull(c.getDownloadUrl());
|
||||
assertNotNull(c.getOwner());
|
||||
assertEquals("jquery/jquery",c.getOwner().getFullName());
|
||||
assertTrue(r.getTotalCount() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void notifications() throws Exception {
|
||||
boolean found=false;
|
||||
for (GHThread t : gitHub.listNotifications().nonBlocking(true).read(true)) {
|
||||
if (!found) {
|
||||
found = true;
|
||||
t.markAsRead(); // test this by calling it once on old nofication
|
||||
}
|
||||
assertNotNull(t.getTitle());
|
||||
assertNotNull(t.getReason());
|
||||
|
||||
System.out.println(t.getTitle());
|
||||
System.out.println(t.getLastReadAt());
|
||||
System.out.println(t.getType());
|
||||
System.out.println();
|
||||
}
|
||||
assertTrue(found);
|
||||
gitHub.listNotifications().markAsRead();
|
||||
}
|
||||
|
||||
private void kohsuke() {
|
||||
String login = getUser().getLogin();
|
||||
Assume.assumeTrue(login.equals("kohsuke") || login.equals("kohsuke2"));
|
||||
|
||||
@@ -98,15 +98,16 @@ public class GitHubTest extends TestCase {
|
||||
props.put("customLogin", "bogusLogin");
|
||||
props.put("customOauth", "bogusOauth");
|
||||
props.put("customPassword", "bogusPassword");
|
||||
|
||||
props.put("customEndpoint", "bogusEndpoint");
|
||||
|
||||
setupEnvironment(props);
|
||||
|
||||
GitHubBuilder builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth");
|
||||
GitHubBuilder builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
|
||||
|
||||
assertEquals("bogusLogin", builder.user);
|
||||
assertEquals("bogusOauth", builder.oauthToken);
|
||||
assertEquals("bogusPassword", builder.password);
|
||||
|
||||
assertEquals("bogusEndpoint", builder.endpoint);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,4 +43,11 @@ public class RepositoryTest extends AbstractGitHubApiTestBase {
|
||||
private GHRepository getRepository() throws IOException {
|
||||
return gitHub.getOrganization("github-api-test-org").getRepository("jenkins");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listLanguages() throws IOException {
|
||||
GHRepository r = gitHub.getRepository("kohsuke/github-api");
|
||||
String mainLanguage = r.getLanguage();
|
||||
assertTrue(r.listLanguages().containsKey(mainLanguage));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user