Compare commits

...

42 Commits

Author SHA1 Message Date
Kohsuke Kawaguchi
7b4d3a869b [maven-release-plugin] prepare release github-api-1.68 2015-04-19 17:40:35 -07:00
Kohsuke Kawaguchi
eeebb1b59f Added the 'sha' parameter.
Fixes issue #176
2015-04-19 17:25:13 -07:00
Julien HENRY
63136f64b7 Merge pull request #174
The merge was done manually because the original commit contains lots of
whitespace only changes.
2015-04-19 17:22:04 -07:00
Kohsuke Kawaguchi
1d2fbf2d92 Merge pull request #179 from lskillen/master
Fix NullPointerException on RateLimitHandler when handling API errors.
2015-04-19 17:03:50 -07:00
Kohsuke Kawaguchi
1e52dded14 Added a helper method 2015-04-15 08:33:18 -07:00
Lee Skillen
cfc7005275 Fix NullPointerException on RateLimiter when handling API errors. 2015-04-14 14:45:47 +01:00
Kohsuke Kawaguchi
f23afcd5aa [maven-release-plugin] prepare for next development iteration 2015-04-13 18:30:43 -07:00
Kohsuke Kawaguchi
4e88a0c91b [maven-release-plugin] prepare release github-api-1.67 2015-04-13 18:30:40 -07:00
Kohsuke Kawaguchi
d070f9deb0 TAB -> WS 2015-04-13 18:25:44 -07:00
Kohsuke Kawaguchi
b736e20a74 Added more getHtmlUrl() methods 2015-04-13 18:17:37 -07:00
Kohsuke Kawaguchi
aad20d0a03 Merge pull request #169 from KostyaSha/fixAuthLoop
Throw error for bad creds
2015-04-13 16:55:58 -07:00
Kohsuke Kawaguchi
7ff97348d9 Merge pull request #170 from KostyaSha/coverity
Improvements
2015-04-13 16:55:24 -07:00
Kohsuke Kawaguchi
68dda3a46d Merge pull request #175 2015-04-13 16:47:32 -07:00
Kohsuke Kawaguchi
2cd44f8c33 Added the pair method 2015-04-13 16:47:25 -07:00
Kohsuke Kawaguchi
9775954aff Massaging the PR.
- need to retrieve the object in full to have all the fields properly populated
- documentation fix, as this method points to the root of the forking chain, not just an upstream.
2015-04-13 16:45:38 -07:00
Kohsuke Kawaguchi
1a071b0b54 Returning null instead of throwing an exception (as a matter of taste) 2015-04-13 16:39:20 -07:00
Kohsuke Kawaguchi
8dcea59c74 Fixed javadoc errors 2015-04-13 16:36:14 -07:00
Kohsuke Kawaguchi
f482f77871 Merge pull request #177 from infm/feat/notif
Added getters for the objects notifications refer to
2015-04-13 16:38:22 -07:00
infm
b058c39ee1 Added getters for the objects notifications refer to 2015-04-09 01:21:38 +03:00
Jason Nichols
b926b6c67f Added the source attribute to GHRepository 2015-04-02 14:54:13 -04:00
Kohsuke Kawaguchi
3fb8e5f799 [maven-release-plugin] prepare for next development iteration 2015-03-24 10:26:48 -07:00
Kohsuke Kawaguchi
277ccb5188 [maven-release-plugin] prepare release github-api-1.66 2015-03-24 10:26:44 -07:00
Kanstantsin Shautsou
9ebc9c0867 Use FAIL rate-limit handler for tests
Should avoid possible Thread.sleep() for tests execution.
2015-03-23 21:14:42 +03:00
Kanstantsin Shautsou
f1f96713a4 [CID-107552] Unintended regular expression
regex_expected: The . character(s) in the pattern ".md" can match any character, because calls to replaceAll treat the pattern as a regular expression, which might be unexpected.
2015-03-23 02:35:55 +03:00
Kanstantsin Shautsou
fc3b6d2c2e [CID-107535] Missing call to superclass
Similar to other events
2015-03-23 02:23:44 +03:00
Kanstantsin Shautsou
d0d0716b3b Throw error for bad creds 2015-03-23 02:09:23 +03:00
Kohsuke Kawaguchi
73119afeff [maven-release-plugin] prepare for next development iteration 2015-03-22 15:57:32 -07:00
Kohsuke Kawaguchi
8939179be8 [maven-release-plugin] prepare release github-api-1.65 2015-03-22 15:57:29 -07:00
Kohsuke Kawaguchi
adba2e68db Renamed for consistency with other methods 2015-03-22 15:54:53 -07:00
Kohsuke Kawaguchi
0ef8b471a3 Added subscription related methods 2015-03-22 15:54:10 -07:00
Kohsuke Kawaguchi
205950fc5f Method to mark the thread as read 2015-03-22 15:50:32 -07:00
Kohsuke Kawaguchi
8835b2c745 added a method to mark all the notifications as read 2015-03-22 15:45:36 -07:00
Kohsuke Kawaguchi
74fda40764 Implemented initial notification API support.
Fixes issue #119
2015-03-22 15:40:53 -07:00
Kohsuke Kawaguchi
687a36937e Keep HttpURLConnection() in the field.
The primary motivation was to expose response headers, but this also made the code most concise by reducing the # of parameters that are passed around.
2015-03-22 14:52:34 -07:00
Kohsuke Kawaguchi
2c7b8bd6e8 report error stream even for 404 2015-03-22 14:46:38 -07:00
Kohsuke Kawaguchi
e9417f5fa1 Described how to set up persistent disk cache
This is good enough "fix" for issue #168.
2015-03-22 12:13:30 -07:00
Kohsuke Kawaguchi
5e08b34c43 added code search 2015-03-22 12:08:53 -07:00
Kohsuke Kawaguchi
7b436ffb3b support on-demand data population for the use in code search API. 2015-03-22 12:02:08 -07:00
Kohsuke Kawaguchi
1ee2ec3728 Added repository search 2015-03-22 11:48:56 -07:00
Kohsuke Kawaguchi
ed28768146 implemented user search 2015-03-22 11:41:25 -07:00
Kohsuke Kawaguchi
f931835176 refactored to introduce other search builders 2015-03-22 11:32:48 -07:00
Kohsuke Kawaguchi
0cf9bc2814 [maven-release-plugin] prepare for next development iteration 2015-03-22 11:16:03 -07:00
44 changed files with 1820 additions and 668 deletions

View File

@@ -7,7 +7,7 @@
</parent> </parent>
<artifactId>github-api</artifactId> <artifactId>github-api</artifactId>
<version>1.64-SNAPSHOT</version> <version>1.68</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>HEAD</tag> <tag>github-api-1.68</tag>
</scm> </scm>
<distributionManagement> <distributionManagement>

View File

@@ -1,6 +1,7 @@
package org.kohsuke.github; package org.kohsuke.github;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
/** /**
* Asset in a release. * Asset in a release.
@@ -60,6 +61,14 @@ public class GHAsset extends GHObject {
return state; return state;
} }
/**
* @deprecated This object has no HTML URL.
*/
@Override
public URL getHtmlUrl() {
return null;
}
public String getBrowserDownloadUrl() { public String getBrowserDownloadUrl() {
return browser_download_url; return browser_download_url;
} }

View File

@@ -10,7 +10,7 @@ import java.util.List;
* *
* @author janinko * @author janinko
* @see GitHub#createToken(Collection, String, String) * @see GitHub#createToken(Collection, String, String)
* @see http://developer.github.com/v3/oauth/#create-a-new-authorization * @see <a href="http://developer.github.com/v3/oauth/#create-a-new-authorization">API documentation</a>
*/ */
public class GHAuthorization extends GHObject { public class GHAuthorization extends GHObject {
public static final String USER = "user"; public static final String USER = "user";
@@ -63,6 +63,14 @@ public class GHAuthorization extends GHObject {
return GitHub.parseURL(url); return GitHub.parseURL(url);
} }
/**
* @deprecated This object has no HTML URL.
*/
@Override
public URL getHtmlUrl() {
return null;
}
public String getNote() { public String getNote() {
return note; return note;
} }
@@ -77,9 +85,6 @@ public class GHAuthorization extends GHObject {
} }
private static class App { private static class App {
private String url; private String url;
private String name; private String name;

View File

@@ -155,7 +155,6 @@ public class GHCommit {
User author,committer; User author,committer;
public ShortInfo getCommitShortInfo() { public ShortInfo getCommitShortInfo() {
return commit; return commit;
} }

View File

@@ -23,6 +23,8 @@
*/ */
package org.kohsuke.github; package org.kohsuke.github;
import java.io.IOException;
/** /**
* Identifies a commit in {@link GHPullRequest}. * Identifies a commit in {@link GHPullRequest}.
* *
@@ -69,6 +71,13 @@ public class GHCommitPointer {
return label; return label;
} }
/**
* Obtains the commit that this pointer is referring to.
*/
public GHCommit getCommit() throws IOException {
return getRepository().getCommit(getSha());
}
void wrapUp(GitHub root) { void wrapUp(GitHub root) {
if (user!=null) user.root = root; if (user!=null) user.root = root;
if (repo!=null) repo.wrap(root); if (repo!=null) repo.wrap(root);

View File

@@ -1,14 +1,15 @@
package org.kohsuke.github; package org.kohsuke.github;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.util.Date; import java.util.Date;
/** /**
* Represents a status of a commit. * Represents a status of a commit.
* *
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
* @see GHRepository#getCommitStatus(String) * @see GHRepository#getLastCommitStatus(String)
* @see GHCommit#getStatus() * @see GHCommit#getLastStatus()
*/ */
public class GHCommitStatus extends GHObject { public class GHCommitStatus extends GHObject {
String state; String state;
@@ -52,4 +53,12 @@ public class GHCommitStatus extends GHObject {
public String getContext() { public String getContext() {
return context; return context;
} }
/**
* @deprecated This object has no HTML URL.
*/
@Override
public URL getHtmlUrl() {
return null;
}
} }

View File

@@ -16,7 +16,13 @@ import javax.xml.bind.DatatypeConverter;
*/ */
@SuppressWarnings({"UnusedDeclaration"}) @SuppressWarnings({"UnusedDeclaration"})
public class GHContent { 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 type;
private String encoding; private String encoding;
@@ -31,7 +37,7 @@ public class GHContent {
private String download_url; private String download_url;
public GHRepository getOwner() { public GHRepository getOwner() {
return owner; return repository;
} }
public String getType() { public String getType() {
@@ -107,13 +113,16 @@ 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(owner.root).read(getDownloadUrl()); return new Requester(root).asStream(getDownloadUrl());
} }
/** /**
* URL to retrieve the raw content of the file. Null if this is a directory. * URL to retrieve the raw content of the file. Null if this is a directory.
*/ */
public String getDownloadUrl() { return download_url; } public String getDownloadUrl() throws IOException {
populate();
return download_url;
}
public boolean isFile() { public boolean isFile() {
return "file".equals(type); return "file".equals(type);
@@ -123,6 +132,16 @@ public class GHContent {
return "dir".equals(type); 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. * List immediate children of this directory.
*/ */
@@ -132,10 +151,10 @@ public class GHContent {
return new PagedIterable<GHContent>() { return new PagedIterable<GHContent>() {
public PagedIterator<GHContent> iterator() { 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 @Override
protected void wrapUp(GHContent[] page) { protected void wrapUp(GHContent[] page) {
GHContent.wrap(page,owner); GHContent.wrap(page, repository);
} }
}; };
} }
@@ -157,7 +176,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 = DatatypeConverter.printBase64Binary(newContentBytes);
Requester requester = new Requester(owner.root) Requester requester = new Requester(root)
.with("path", path) .with("path", path)
.with("message", commitMessage) .with("message", commitMessage)
.with("sha", sha) .with("sha", sha)
@@ -170,8 +189,8 @@ public class GHContent {
GHContentUpdateResponse response = requester.to(getApiRoute(), GHContentUpdateResponse.class); GHContentUpdateResponse response = requester.to(getApiRoute(), GHContentUpdateResponse.class);
response.getContent().wrap(owner); response.getContent().wrap(repository);
response.getCommit().wrapUp(owner); response.getCommit().wrapUp(repository);
this.content = encodedContent; this.content = encodedContent;
return response; return response;
@@ -182,7 +201,7 @@ public class GHContent {
} }
public GHContentUpdateResponse delete(String commitMessage, String branch) throws IOException { public GHContentUpdateResponse delete(String commitMessage, String branch) throws IOException {
Requester requester = new Requester(owner.root) Requester requester = new Requester(root)
.with("path", path) .with("path", path)
.with("message", commitMessage) .with("message", commitMessage)
.with("sha", sha) .with("sha", sha)
@@ -194,18 +213,26 @@ public class GHContent {
GHContentUpdateResponse response = requester.to(getApiRoute(), GHContentUpdateResponse.class); GHContentUpdateResponse response = requester.to(getApiRoute(), GHContentUpdateResponse.class);
response.getCommit().wrapUp(owner); response.getCommit().wrapUp(repository);
return response; return response;
} }
private String getApiRoute() { private String getApiRoute() {
return "/repos/" + owner.getOwnerName() + "/" + owner.getName() + "/contents/" + path; return "/repos/" + repository.getOwnerName() + "/" + repository.getName() + "/contents/" + path;
} }
GHContent wrap(GHRepository owner) { GHContent wrap(GHRepository owner) {
this.owner = owner; this.repository = owner;
this.root = owner.root;
return this; 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) { public static GHContent[] wrap(GHContent[] contents, GHRepository repository) {
for (GHContent unwrappedContent : contents) { for (GHContent unwrappedContent : contents) {

View 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";
}
}

View File

@@ -50,4 +50,12 @@ public class GHDeployment extends GHObject {
public String getSha(){ public String getSha(){
return sha; return sha;
} }
/**
* @deprecated This object has no HTML URL.
*/
@Override
public URL getHtmlUrl() {
return null;
}
} }

View File

@@ -32,5 +32,11 @@ public class GHDeploymentStatus extends GHObject {
return GHDeploymentState.valueOf(state.toUpperCase()); return GHDeploymentState.valueOf(state.toUpperCase());
} }
/**
* @deprecated This object has no HTML URL.
*/
@Override
public URL getHtmlUrl() {
return null;
}
} }

View File

@@ -165,6 +165,7 @@ public abstract class GHEventPayload {
@Override @Override
void wrapUp(GitHub root) { void wrapUp(GitHub root) {
super.wrapUp(root);
if (repository!=null) if (repository!=null)
repository.wrap(root); repository.wrap(root);
} }

View File

@@ -3,6 +3,7 @@ package org.kohsuke.github;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -60,8 +61,8 @@ public class GHGist extends GHObject {
return git_push_url; return git_push_url;
} }
public String getHtmlUrl() { public URL getHtmlUrl() {
return html_url; return GitHub.parseURL(html_url);
} }
public boolean isPublic() { public boolean isPublic() {

View File

@@ -1,6 +1,7 @@
package org.kohsuke.github; package org.kohsuke.github;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
@@ -51,4 +52,12 @@ public class GHHook extends GHObject {
public void delete() throws IOException { public void delete() throws IOException {
new Requester(repository.root).method("DELETE").to(String.format("/repos/%s/%s/hooks/%d", repository.getOwnerName(), repository.getName(), id)); new Requester(repository.root).method("DELETE").to(String.format("/repos/%s/%s/hooks/%d", repository.getOwnerName(), repository.getName(), id));
} }
/**
* @deprecated This object has no HTML URL.
*/
@Override
public URL getHtmlUrl() {
return null;
}
} }

View File

@@ -71,4 +71,12 @@ public class GHIssueComment extends GHObject {
public GHUser getUser() throws IOException { public GHUser getUser() throws IOException {
return owner.root.getUser(user.getLogin()); return owner.root.getUser(user.getLogin());
} }
/**
* @deprecated This object has no HTML URL.
*/
@Override
public URL getHtmlUrl() {
return null;
}
} }

View File

@@ -1,9 +1,5 @@
package org.kohsuke.github; package org.kohsuke.github;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
@@ -12,21 +8,16 @@ import java.util.Locale;
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
* @see GitHub#searchIssues() * @see GitHub#searchIssues()
*/ */
public class GHIssueSearchBuilder { public class GHIssueSearchBuilder extends GHSearchBuilder<GHIssue> {
private final GitHub root;
private final Requester req;
private final List<String> terms = new ArrayList<String>();
/*package*/ GHIssueSearchBuilder(GitHub root) { /*package*/ GHIssueSearchBuilder(GitHub root) {
this.root = root; super(root,IssueSearchResult.class);
req = root.retrieve();
} }
/** /**
* Search terms. * Search terms.
*/ */
public GHIssueSearchBuilder q(String term) { public GHIssueSearchBuilder q(String term) {
terms.add(term); super.q(term);
return this; return this;
} }
@@ -61,25 +52,15 @@ public class GHIssueSearchBuilder {
private GHIssue[] items; private GHIssue[] items;
@Override @Override
public GHIssue[] getItems() { /*package*/ GHIssue[] getItems(GitHub root) {
for (GHIssue i : items)
i.wrap(root);
return items; return items;
} }
} }
/** @Override
* Lists up the issues with the criteria built so far. protected String getApiUrl() {
*/ return "/search/issues";
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);
}
};
}
};
} }
} }

View File

@@ -1,6 +1,7 @@
package org.kohsuke.github; package org.kohsuke.github;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
@@ -14,7 +15,7 @@ public class GHMilestone extends GHObject {
GHRepository owner; GHRepository owner;
GHUser creator; GHUser creator;
private String state, due_on, title, description; private String state, due_on, title, description, html_url;
private int closed_issues, open_issues, number; private int closed_issues, open_issues, number;
public GitHub getRoot() { public GitHub getRoot() {
@@ -54,6 +55,10 @@ public class GHMilestone extends GHObject {
return number; return number;
} }
public URL getHtmlUrl() {
return GitHub.parseURL(html_url);
}
public GHMilestoneState getState() { public GHMilestoneState getState() {
return Enum.valueOf(GHMilestoneState.class, state.toUpperCase(Locale.ENGLISH)); return Enum.valueOf(GHMilestoneState.class, state.toUpperCase(Locale.ENGLISH));
} }

View 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];
}

View File

@@ -38,6 +38,12 @@ public abstract class GHObject {
return GitHub.parseURL(url); return GitHub.parseURL(url);
} }
/**
* URL of this object for humans, which renders some HTML.
*/
@WithBridgeMethods(value=String.class, adapterMethod="urlToString")
public abstract URL getHtmlUrl();
/** /**
* When was this resource last updated? * When was this resource last updated?
*/ */

View File

@@ -2,6 +2,7 @@ package org.kohsuke.github;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@@ -214,8 +215,9 @@ public abstract class GHPerson extends GHObject {
return blog; return blog;
} }
public String getHtmlUrl() { @Override
return html_url; public URL getHtmlUrl() {
return GitHub.parseURL(html_url);
} }
/** /**

View File

@@ -27,7 +27,6 @@ import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.Locale;
/** /**
* A pull request. * A pull request.
@@ -197,6 +196,39 @@ public class GHPullRequest extends GHIssue {
root.retrieve().to(url, this).wrapUp(owner); root.retrieve().to(url, this).wrapUp(owner);
} }
/**
* Retrieves all the commits associated to this pull request.
*/
public PagedIterable<GHPullRequestFileDetail> listFiles() {
return new PagedIterable<GHPullRequestFileDetail>() {
public PagedIterator<GHPullRequestFileDetail> iterator() {
return new PagedIterator<GHPullRequestFileDetail>(root.retrieve().asIterator(String.format("%s/files", getApiURL()),
GHPullRequestFileDetail[].class)) {
@Override
protected void wrapUp(GHPullRequestFileDetail[] page) {
}
};
}
};
}
/**
* Obtains all the review comments associated with this pull request.
*/
public PagedIterable<GHPullRequestReviewComment> listReviewComments() throws IOException {
return new PagedIterable<GHPullRequestReviewComment>() {
public PagedIterator<GHPullRequestReviewComment> iterator() {
return new PagedIterator<GHPullRequestReviewComment>(root.retrieve().asIterator(getApiRoute() + "/comments",
GHPullRequestReviewComment[].class)) {
protected void wrapUp(GHPullRequestReviewComment[] page) {
for (GHPullRequestReviewComment c : page)
c.wrapUp(GHPullRequest.this);
}
};
}
};
}
/** /**
* Retrieves all the commits associated to this pull request. * Retrieves all the commits associated to this pull request.
*/ */
@@ -208,12 +240,23 @@ public class GHPullRequest extends GHIssue {
GHPullRequestCommitDetail[].class)) { GHPullRequestCommitDetail[].class)) {
@Override @Override
protected void wrapUp(GHPullRequestCommitDetail[] page) { protected void wrapUp(GHPullRequestCommitDetail[] page) {
for (GHPullRequestCommitDetail c : page)
c.wrapUp(GHPullRequest.this);
} }
}; };
} }
}; };
} }
public GHPullRequestReviewComment createReviewComment(String body, String sha, String path, int position) throws IOException {
return new Requester(root).method("POST")
.with("body", body)
.with("commit_id", sha)
.with("path", path)
.with("position", position)
.to(getApiRoute() + "/comments", GHPullRequestReviewComment.class).wrapUp(this);
}
/** /**
* Merge this pull request. * Merge this pull request.
* *
@@ -223,7 +266,21 @@ public class GHPullRequest extends GHIssue {
* Commit message. If null, the default one will be used. * Commit message. If null, the default one will be used.
*/ */
public void merge(String msg) throws IOException { public void merge(String msg) throws IOException {
new Requester(root).method("PUT").with("commit_message",msg).to(getApiRoute()+"/merge"); merge(msg,null);
}
/**
* Merge this pull request.
*
* The equivalent of the big green "Merge pull request" button.
*
* @param msg
* Commit message. If null, the default one will be used.
* @param sha
* SHA that pull request head must match to allow merge.
*/
public void merge(String msg, String sha) throws IOException {
new Requester(root).method("PUT").with("commit_message",msg).with("sha",sha).to(getApiRoute()+"/merge");
} }
private void fetchIssue() throws IOException { private void fetchIssue() throws IOException {

View File

@@ -31,13 +31,20 @@ import java.net.URL;
* Commit detail inside a {@link GHPullRequest}. * Commit detail inside a {@link GHPullRequest}.
* *
* @author Luca Milanesio * @author Luca Milanesio
* @see GHPullRequest#listCommits()
*/ */
public class GHPullRequestCommitDetail { public class GHPullRequestCommitDetail {
private GHPullRequest owner;
/*package*/ void wrapUp(GHPullRequest owner) {
this.owner = owner;
}
/** /**
* @deprecated Use {@link GitUser} * @deprecated Use {@link GitUser}
*/ */
public static class Authorship extends GitUser {} public static class Authorship extends GitUser {
}
public static class Tree { public static class Tree {
String sha; String sha;
@@ -111,18 +118,23 @@ public class GHPullRequestCommitDetail {
public String getSha() { public String getSha() {
return sha; return sha;
} }
public Commit getCommit() { public Commit getCommit() {
return commit; return commit;
} }
public URL getApiUrl() { public URL getApiUrl() {
return GitHub.parseURL(url); return GitHub.parseURL(url);
} }
public URL getUrl() { public URL getUrl() {
return GitHub.parseURL(html_url); return GitHub.parseURL(html_url);
} }
public URL getCommentsUrl() { public URL getCommentsUrl() {
return GitHub.parseURL(comments_url); return GitHub.parseURL(comments_url);
} }
public CommitPointer[] getParents() { public CommitPointer[] getParents() {
return parents; return parents;
} }

View File

@@ -0,0 +1,86 @@
/*
* The MIT License
*
* Copyright (c) 2015, Julien Henry
*
* 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 java.net.URL;
/**
* File detail inside a {@link GHPullRequest}.
*
* @author Julien Henry
* @see GHPullRequest#listFiles()
*/
public class GHPullRequestFileDetail {
String sha;
String filename;
String status;
int additions;
int deletions;
int changes;
String blob_url;
String raw_url;
String contents_url;
String patch;
public String getSha() {
return sha;
}
public String getFilename() {
return filename;
}
public String getStatus() {
return status;
}
public int getAdditions() {
return additions;
}
public int getDeletions() {
return deletions;
}
public int getChanges() {
return changes;
}
public URL getBlobUrl() {
return GitHub.parseURL(blob_url);
}
public URL getRawUrl() {
return GitHub.parseURL(raw_url);
}
public URL getContentsUrl() {
return GitHub.parseURL(contents_url);
}
public String getPatch() {
return patch;
}
}

View File

@@ -0,0 +1,105 @@
/*
* The MIT License
*
* Copyright (c) 2010, Kohsuke Kawaguchi
*
* 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 java.io.IOException;
import java.net.URL;
/**
* Review comment to the pull request
*
* @author Julien Henry
* @see GHPullRequest#listReviewComments()
* @see GHPullRequest#createReviewComment(String, String, String, int)
*/
public class GHPullRequestReviewComment extends GHObject {
GHPullRequest owner;
private String body;
private GHUser user;
private String path;
private int position;
private int originalPosition;
/*package*/ GHPullRequestReviewComment wrapUp(GHPullRequest owner) {
this.owner = owner;
return this;
}
/**
* Gets the pull request to which this review comment is associated.
*/
public GHPullRequest getParent() {
return owner;
}
/**
* The comment itself.
*/
public String getBody() {
return body;
}
/**
* Gets the user who posted this comment.
*/
public GHUser getUser() throws IOException {
return owner.root.getUser(user.getLogin());
}
public String getPath() {
return path;
}
public int getPosition() {
return position;
}
public int getOriginalPosition() {
return originalPosition;
}
@Override
public URL getHtmlUrl() {
return null;
}
protected String getApiRoute() {
return "/repos/"+owner.getRepository().getFullName()+"/comments/"+id;
}
/**
* Updates the comment.
*/
public void update(String body) throws IOException {
new Requester(owner.root).method("PATCH").with("body", body).to(getApiRoute(),this);
}
/**
* Deletes this review comment.
*/
public void delete() throws IOException {
new Requester(owner.root).method("DELETE").to(getApiRoute());
}
}

View File

@@ -3,6 +3,7 @@ package org.kohsuke.github;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
@@ -44,8 +45,8 @@ public class GHRelease extends GHObject {
return draft; return draft;
} }
public String getHtmlUrl() { public URL getHtmlUrl() {
return html_url; return GitHub.parseURL(html_url);
} }
public String getName() { public String getName() {

View File

@@ -63,6 +63,8 @@ public class GHRepository extends GHObject {
private GHRepoPermission permissions; private GHRepoPermission permissions;
private GHRepository source, parent;
public GHDeploymentBuilder createDeployment(String ref) { public GHDeploymentBuilder createDeployment(String ref) {
return new GHDeploymentBuilder(this,ref); return new GHDeploymentBuilder(this,ref);
} }
@@ -159,6 +161,10 @@ public class GHRepository extends GHObject {
return ssh_url; return ssh_url;
} }
public URL getHtmlUrl() {
return GitHub.parseURL(html_url);
}
/** /**
* Short repository name without the owner. For example 'jenkins' in case of http://github.com/jenkinsci/jenkins * Short repository name without the owner. For example 'jenkins' in case of http://github.com/jenkinsci/jenkins
*/ */
@@ -353,7 +359,7 @@ public class GHRepository extends GHObject {
} }
/** /**
* Returns the primary branch you'll configure in the "Admin > Options" config page. * Returns the primary branch you'll configure in the "Admin &gt; Options" config page.
* *
* @return * @return
* This field is null until the user explicitly configures the master branch. * This field is null until the user explicitly configures the master branch.
@@ -780,7 +786,7 @@ public class GHRepository extends GHObject {
} }
/** /**
* @see {@link #createCommitStatus(String, GHCommitState,String,String,String) createCommitStatus} * @see #createCommitStatus(String, GHCommitState,String,String,String)
*/ */
public GHCommitStatus createCommitStatus(String sha1, GHCommitState state, String targetUrl, String description) throws IOException { public GHCommitStatus createCommitStatus(String sha1, GHCommitState state, String targetUrl, String description) throws IOException {
return createCommitStatus(sha1, state, targetUrl, description,null); return createCommitStatus(sha1, state, targetUrl, description,null);
@@ -1107,6 +1113,38 @@ public class GHRepository extends GHObject {
return list; return list;
} }
/**
* Forked repositories have a 'source' attribute that specifies the ultimate source of the forking chain.
*
* @return
* {@link GHRepository} that points to the root repository where this repository is forked
* (indirectly or directly) from. Otherwise null.
* @see #getParent()
*/
public GHRepository getSource() throws IOException {
if (source == null) return null;
if (source.root == null)
source = root.getRepository(source.getFullName());
return source;
}
/**
* Forked repositories have a 'parent' attribute that specifies the repository this repository
* is directly forked from. If we keep traversing {@link #getParent()} until it returns null, that
* is {@link #getSource()}.
*
* @return
* {@link GHRepository} that points to the repository where this repository is forked
* directly from. Otherwise null.
* @see #getSource()
*/
public GHRepository getParent() throws IOException {
if (parent == null) return null;
if (parent.root == null)
parent = root.getRepository(parent.getFullName());
return parent;
}
/** /**
* Subscribes to this repository to get notifications. * Subscribes to this repository to get notifications.
*/ */
@@ -1166,10 +1204,16 @@ public class GHRepository extends GHObject {
.with("text", text) .with("text", text)
.with("mode",mode==null?null:mode.toString()) .with("mode",mode==null?null:mode.toString())
.with("context", getFullName()) .with("context", getFullName())
.read("/markdown"), .asStream("/markdown"),
"UTF-8"); "UTF-8");
} }
/**
* List all the notifications in a repository for the current user.
*/
public GHNotificationStream listNotifications() {
return new GHNotificationStream(root,getApiTailUrl("/notifications"));
}
@Override @Override

View File

@@ -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";
}
}

View 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();
}

View File

@@ -4,9 +4,11 @@ import java.io.IOException;
import java.util.Date; import java.util.Date;
/** /**
* Represents your subscribing to a repository. * Represents your subscribing to a repository / conversation thread..
* *
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
* @see GHRepository#getSubscription()
* @see GHThread#getSubscription()
*/ */
public class GHSubscription { public class GHSubscription {
private String created_at, url, repository_url, reason; private String created_at, url, repository_url, reason;

View File

@@ -0,0 +1,142 @@
package org.kohsuke.github;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
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);
}
/**
* @deprecated This object has no HTML URL.
*/
@Override
public URL getHtmlUrl() {
return null;
}
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;
}
/**
* If this thread is about an issue, return that issue.
*
* @return null if this thread is not about an issue.
*/
public GHIssue getBoundIssue() throws IOException {
if (!"Issue".equals(subject.type) && "PullRequest".equals(subject.type))
return null;
return repository.getIssue(
Integer.parseInt(subject.url.substring(subject.url.lastIndexOf('/') + 1)));
}
/**
* If this thread is about a pull request, return that pull request.
*
* @return null if this thread is not about a pull request.
*/
public GHPullRequest getBoundPullRequest() throws IOException {
if (!"PullRequest".equals(subject.type))
return null;
return repository.getPullRequest(
Integer.parseInt(subject.url.substring(subject.url.lastIndexOf('/') + 1)));
}
/**
* If this thread is about a commit, return that commit.
*
* @return null if this thread is not about a commit.
*/
public GHCommit getBoundCommit() throws IOException {
if (!"Commit".equals(subject.type))
return null;
return repository.getCommit(subject.url.substring(subject.url.lastIndexOf('/') + 1));
}
/*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;
}
}
}

View 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";
}
}

View File

@@ -60,7 +60,7 @@ import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
* <p> * <p>
* This library aims to be safe for use by multiple threads concurrently, although * This library aims to be safe for use by multiple threads concurrently, although
* the library itself makes no attempt to control/serialize potentially conflicting * the library itself makes no attempt to control/serialize potentially conflicting
* operations to GitHub, such as updating & deleting a repository at the same time. * operations to GitHub, such as updating &amp; deleting a repository at the same time.
* *
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
@@ -133,10 +133,11 @@ public class GitHub {
} }
} }
this.rateLimitHandler = rateLimitHandler;
if (login==null && encodedAuthorization!=null) if (login==null && encodedAuthorization!=null)
login = getMyself().getLogin(); login = getMyself().getLogin();
this.login = login; this.login = login;
this.rateLimitHandler = rateLimitHandler;
} }
/** /**
@@ -443,6 +444,34 @@ public class GitHub {
return new GHIssueSearchBuilder(this); 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. * 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> * @see <a href="https://developer.github.com/v3/repos/#list-all-public-repositories">documentation</a>
@@ -487,7 +516,7 @@ public class GitHub {
new Requester(this) new Requester(this)
.with(new ByteArrayInputStream(text.getBytes("UTF-8"))) .with(new ByteArrayInputStream(text.getBytes("UTF-8")))
.contentType("text/plain;charset=UTF-8") .contentType("text/plain;charset=UTF-8")
.read("/markdown/raw"), .asStream("/markdown/raw"),
"UTF-8"); "UTF-8");
} }

View File

@@ -8,11 +8,17 @@ import java.util.Iterator;
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
public abstract class PagedSearchIterable<T> extends PagedIterable<T> { 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. * As soon as we have any result fetched, it's set here so that we can report the total count.
*/ */
private SearchResult<T> result; 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. * 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() { public T[] next() {
SearchResult<T> v = base.next(); SearchResult<T> v = base.next();
if (result==null) result = v; if (result==null) result = v;
return v.getItems(); return v.getItems(root);
} }
public void remove() { public void remove() {

View File

@@ -23,7 +23,8 @@
*/ */
package org.kohsuke.github; 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.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@@ -43,6 +44,7 @@ import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
@@ -51,8 +53,7 @@ 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;
import com.fasterxml.jackson.databind.JsonMappingException; import static org.kohsuke.github.GitHub.*;
import org.apache.commons.io.IOUtils;
/** /**
* A builder pattern for making HTTP call and parsing its output. * A builder pattern for making HTTP call and parsing its output.
@@ -62,6 +63,7 @@ import org.apache.commons.io.IOUtils;
class Requester { class Requester {
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>();
/** /**
* Request method. * Request method.
@@ -70,6 +72,11 @@ class Requester {
private String contentType = "application/x-www-form-urlencoded"; private String contentType = "application/x-www-form-urlencoded";
private InputStream body; private InputStream body;
/**
* Current connection.
*/
private HttpURLConnection uc;
private static class Entry { private static class Entry {
String key; String key;
Object value; Object value;
@@ -84,6 +91,15 @@ class Requester {
this.root = root; 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. * Makes a request with authentication credential.
*/ */
@@ -107,6 +123,10 @@ class Requester {
public Requester with(String key, boolean value) { public Requester with(String key, boolean value) {
return _with(key, value); return _with(key, value);
} }
public Requester with(String key, Boolean value) {
return _with(key, value);
}
public Requester with(String key, String value) { public Requester with(String key, String value) {
return _with(key, value); return _with(key, value);
@@ -196,12 +216,12 @@ class Requester {
} }
tailApiUrl += qs.toString(); tailApiUrl += qs.toString();
} }
HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl)); setupConnection(root.getApiURL(tailApiUrl));
buildRequest(uc); buildRequest();
try { 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 if (type != null && type.isArray()) { // we might have to loop for pagination - done through recursion
final String links = uc.getHeaderField("link"); final String links = uc.getHeaderField("link");
if (links != null && links.contains("rel=\"next\"")) { if (links != null && links.contains("rel=\"next\"")) {
@@ -222,7 +242,7 @@ class Requester {
} }
return result; return result;
} catch (IOException e) { } catch (IOException e) {
handleApiError(e,uc); handleApiError(e);
} }
} }
} }
@@ -232,33 +252,41 @@ 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
HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl)); setupConnection(root.getApiURL(tailApiUrl));
buildRequest(uc); buildRequest();
try { try {
return uc.getResponseCode(); return uc.getResponseCode();
} catch (IOException e) { } catch (IOException e) {
handleApiError(e,uc); handleApiError(e);
} }
} }
} }
public InputStream read(String tailApiUrl) throws IOException { public InputStream asStream(String tailApiUrl) throws IOException {
while (true) {// loop while API rate limit is hit while (true) {// loop while API rate limit is hit
HttpURLConnection uc = setupConnection(root.getApiURL(tailApiUrl)); setupConnection(root.getApiURL(tailApiUrl));
buildRequest(uc); buildRequest();
try { try {
return wrapStream(uc,uc.getInputStream()); return wrapStream(uc.getInputStream());
} catch (IOException e) { } catch (IOException e) {
handleApiError(e,uc); handleApiError(e);
} }
} }
} }
private void buildRequest(HttpURLConnection uc) throws IOException { 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")) { if (!method.equals("GET")) {
uc.setDoOutput(true); uc.setDoOutput(true);
uc.setRequestProperty("Content-type", contentType); uc.setRequestProperty("Content-type", contentType);
@@ -347,14 +375,14 @@ class Requester {
try { try {
while (true) {// loop while API rate limit is hit while (true) {// loop while API rate limit is hit
HttpURLConnection uc = setupConnection(url); setupConnection(url);
try { try {
next = parse(uc,type,null); next = parse(type,null);
assert next!=null; assert next!=null;
findNextURL(uc); findNextURL();
return; return;
} catch (IOException e) { } catch (IOException e) {
handleApiError(e,uc); handleApiError(e);
} }
} }
} catch (IOException e) { } catch (IOException e) {
@@ -365,7 +393,7 @@ class Requester {
/** /**
* Locate the next page from the pagination "Link" tag. * 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 url = null; // start defensively
String link = uc.getHeaderField("Link"); String link = uc.getHeaderField("Link");
if (link==null) return; if (link==null) return;
@@ -386,14 +414,20 @@ class Requester {
} }
private HttpURLConnection setupConnection(URL url) throws IOException { private void setupConnection(URL url) throws IOException {
HttpURLConnection uc = root.getConnector().connect(url); uc = root.getConnector().connect(url);
// if the authentication is needed but no credential is given, try it anyway (so that some calls // 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.) // that do work with anonymous access in the reduced form should still work.)
if (root.encodedAuthorization!=null) if (root.encodedAuthorization!=null)
uc.setRequestProperty("Authorization", root.encodedAuthorization); 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 { try {
uc.setRequestMethod(method); uc.setRequestMethod(method);
} catch (ProtocolException e) { } catch (ProtocolException e) {
@@ -407,13 +441,14 @@ class Requester {
} }
} }
uc.setRequestProperty("Accept-Encoding", "gzip"); 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; InputStreamReader r = null;
try { try {
r = new InputStreamReader(wrapStream(uc, 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 {
@@ -432,7 +467,7 @@ class Requester {
/** /**
* Handles the "Content-Encoding" header. * 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(); String encoding = uc.getContentEncoding();
if (encoding==null || in==null) return in; if (encoding==null || in==null) return in;
if (encoding.equals("gzip")) return new GZIPInputStream(in); if (encoding.equals("gzip")) return new GZIPInputStream(in);
@@ -443,19 +478,23 @@ 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, HttpURLConnection uc) throws IOException { /*package*/ void handleApiError(IOException e) throws IOException {
if (uc.getResponseCode() == 401) // Unauthorized == bad creds
throw e;
if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) { if ("0".equals(uc.getHeaderField("X-RateLimit-Remaining"))) {
root.rateLimitHandler.onError(e,uc); root.rateLimitHandler.onError(e,uc);
} }
if (e instanceof FileNotFoundException) InputStream es = wrapStream(uc.getErrorStream());
throw e; // pass through 404 Not Found to allow the caller to handle it intelligently
InputStream es = wrapStream(uc, uc.getErrorStream());
try { try {
if (es!=null) 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); throw (IOException) new IOException(IOUtils.toString(es, "UTF-8")).initCause(e);
else } else
throw e; throw e;
} finally { } finally {
IOUtils.closeQuietly(es); IOUtils.closeQuietly(es);

View File

@@ -9,5 +9,8 @@ abstract class SearchResult<T> {
int total_count; int total_count;
boolean incomplete_results; boolean incomplete_results;
public abstract T[] getItems(); /**
* Wraps up the retrieved object and return them. Only called once.
*/
/*package*/ abstract T[] getItems(GitHub root);
} }

View File

@@ -42,3 +42,10 @@ through `HttpConnector`. In particular, this means you can use [OkHttp](http://s
so we can make use of it's HTTP response cache. so we can make use of it's HTTP response cache.
Making a conditional request against the GitHub API and receiving a 304 response 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();

View File

@@ -19,9 +19,9 @@ public abstract class AbstractGitHubApiTestBase extends Assert {
if (f.exists()) { if (f.exists()) {
// use the non-standard credential preferentially, so that developers of this library do not have // use the non-standard credential preferentially, so that developers of this library do not have
// to clutter their event stream. // to clutter their event stream.
gitHub = GitHubBuilder.fromPropertyFile(f.getPath()).build(); gitHub = GitHubBuilder.fromPropertyFile(f.getPath()).withRateLimitHandler(RateLimitHandler.FAIL).build();
} else { } else {
gitHub = GitHub.connect(); gitHub = GitHubBuilder.fromCredentials().withRateLimitHandler(RateLimitHandler.FAIL).build();
} }
} }

View File

@@ -674,7 +674,7 @@ public class AppTest extends AbstractGitHubApiTestBase {
GHTree masterTree = gitHub.getRepository("kohsuke/github-api").getTree("master"); GHTree masterTree = gitHub.getRepository("kohsuke/github-api").getTree("master");
boolean foundReadme = false; boolean foundReadme = false;
for(GHTreeEntry e : masterTree.getTree()){ for(GHTreeEntry e : masterTree.getTree()){
if("readme".equalsIgnoreCase(e.getPath().replaceAll(".md", ""))){ if("readme".equalsIgnoreCase(e.getPath().replaceAll("\\.md", ""))){
foundReadme = true; foundReadme = true;
break; break;
} }
@@ -777,6 +777,56 @@ public class AppTest extends AbstractGitHubApiTestBase {
assertTrue(actual.contains("to fix issue")); 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() { 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

@@ -55,8 +55,7 @@ public class GitHubTest extends TestCase {
* Its used to wire in values for the github credentials to test that the GitHubBuilder works properly to resolve them. * Its used to wire in values for the github credentials to test that the GitHubBuilder works properly to resolve them.
*/ */
private void setupEnvironment(Map<String, String> newenv) { private void setupEnvironment(Map<String, String> newenv) {
try try {
{
Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment"); Class<?> processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment");
Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment"); Field theEnvironmentField = processEnvironmentClass.getDeclaredField("theEnvironment");
theEnvironmentField.setAccessible(true); theEnvironmentField.setAccessible(true);
@@ -66,9 +65,7 @@ public class GitHubTest extends TestCase {
theCaseInsensitiveEnvironmentField.setAccessible(true); theCaseInsensitiveEnvironmentField.setAccessible(true);
Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null); Map<String, String> cienv = (Map<String, String>) theCaseInsensitiveEnvironmentField.get(null);
cienv.putAll(newenv); cienv.putAll(newenv);
} } catch (NoSuchFieldException e) {
catch (NoSuchFieldException e)
{
try { try {
Class[] classes = Collections.class.getDeclaredClasses(); Class[] classes = Collections.class.getDeclaredClasses();
Map<String, String> env = System.getenv(); Map<String, String> env = System.getenv();
@@ -88,11 +85,9 @@ public class GitHubTest extends TestCase {
} catch (Exception e1) { } catch (Exception e1) {
e1.printStackTrace(); e1.printStackTrace();
} }
} }
public void testGitHubBuilderFromCustomEnvironment() throws IOException { public void testGitHubBuilderFromCustomEnvironment() throws IOException {
Map<String, String> props = new HashMap<String, String>(); Map<String, String> props = new HashMap<String, String>();
props.put("customLogin", "bogusLogin"); props.put("customLogin", "bogusLogin");