Compare commits

...

95 Commits

Author SHA1 Message Date
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
Kohsuke Kawaguchi
8b428f2c93 whitespace only changes for consistent indentation 2015-03-22 11:14:19 -07:00
Kohsuke Kawaguchi
10238dbcd3 Explaining why this code is the way it is. 2015-03-22 11:13:40 -07:00
Kohsuke Kawaguchi
6229e0928d Revert "Set credentials file according to documentation"
This reverts commit 0bf81f4fb9.

The point of this is to allow me to use a separate account to avoid
corrupting my event stream. GitHub.connect() does the standard handling
for those who are not me.
2015-03-22 11:11:03 -07:00
Kohsuke Kawaguchi
5c7b259fe9 Using the latest 2015-03-22 11:02:17 -07:00
Kohsuke Kawaguchi
cc84c867c0 I think our coverage is pretty good now 2015-03-22 11:01:02 -07:00
Kohsuke Kawaguchi
5bf252e12d Added markdown support
Fixes issue #165
2015-03-22 11:00:57 -07:00
Kohsuke Kawaguchi
75512ff66a Turns out the interning of GHUser wasn't working at all!
Fixes issue #166.
2015-03-22 10:38:57 -07:00
Kohsuke Kawaguchi
6f4832476a test case for #162 2015-03-22 10:21:16 -07:00
Kohsuke Kawaguchi
86b0d27299 added a method to read content.
This also fixes #162.
2015-03-22 10:21:09 -07:00
Kohsuke Kawaguchi
5a8845f7f6 added a method to return the raw unprocessed body 2015-03-22 09:17:49 -07:00
Kohsuke Kawaguchi
709e47f32f Added getDownloadUrl() method 2015-03-22 09:10:34 -07:00
Kohsuke Kawaguchi
77590b4eb3 eliminate the need for path manipulation and consolidate them to 'with' 2015-03-21 16:52:49 -07:00
Kohsuke Kawaguchi
72fc313135 improved error handling 2015-03-21 16:43:11 -07:00
Kohsuke Kawaguchi
39b32cee2e Implemented /repositories
Fixed issue #157
2015-03-21 16:35:34 -07:00
Kohsuke Kawaguchi
bdcee7c052 Merge pull request #160 with some modifications
Strategy pattern is better for API rate limit handling as the current
behavior is quite valid for batch applications.

I'm not taking OkHttp version change so as not to introduce any unneeded
version requirement.
2015-03-17 07:44:36 -07:00
Kohsuke Kawaguchi
4093e53b5b Implemented a strategy pattern to let the client determine API rate limit behavior.
The default is set to the backward compatible behaviour.
2015-03-17 07:43:51 -07:00
Kanstantsin Shautsou
a4c1c8de24 Provide reset date info for rate limit 2015-03-17 07:31:05 -07:00
Kohsuke Kawaguchi
7ed234c875 Standardize environment variable names
... that are more like typical environment variables. Reasonably unique and upper case.

Deprecate other methods. The point of a connector method is to make sure all clients of the same library uses the same environments, thereby eliminating the pain of setting credentials per app.

Allowing the app to specify the environment variable names defeat this purpose.
2015-03-15 13:17:57 -07:00
Kohsuke Kawaguchi
0359160ac6 Avoided using JDK6 method 2015-03-15 13:07:21 -07:00
Kohsuke Kawaguchi
2478dad9b5 Simplification 2015-03-15 13:04:50 -07:00
Kohsuke Kawaguchi
690292352b Simplification
But this method is insane!
2015-03-15 13:02:28 -07:00
Kohsuke Kawaguchi
271d18cddc simplification 2015-03-15 13:01:35 -07:00
Kohsuke Kawaguchi
e1465639e7 Merge pull request #156 from ashwanthkumar/endpoint-from-properties
Picking endpoint from the properties file and environment variables
2015-03-15 19:55:32 +00:00
Kohsuke Kawaguchi
ce7ca59339 Merge pull request #155 2015-03-15 12:49:30 -07:00
Kohsuke Kawaguchi
76610b25d7 Promoted GHTreeEntry to the top-level 2015-03-15 12:49:14 -07:00
Kohsuke Kawaguchi
dfce0bda7c prefer list over raw array 2015-03-15 12:48:27 -07:00
Ashwanth Kumar
41c0dd9727 Fixing the indentation for enpointVariableName 2015-03-13 21:26:49 +05:30
Ashwanth Kumar
232c0389d3 Adding fromEnvironment to maintain backword compatibility 2015-03-13 21:26:19 +05:30
Oleg Nenashev
d95c8a4ab0 Merge pull request #161 from khoa-nd/master
Add method to get the list of languages using in repository
2015-03-13 17:58:01 +03:00
khoa-nd
374fdb37e1 Change type of language bytes from Integer to Long 2015-03-06 09:11:03 +07:00
khoa-nd
f78530636e Add method to get the list of languages using in repository 2015-03-05 15:53:02 +07:00
Kanstantsin Shautsou
0bf81f4fb9 Set credentials file according to documentation 2015-03-03 19:23:06 +03:00
Kohsuke Kawaguchi
dcc3b7f36b [maven-release-plugin] prepare for next development iteration 2015-03-02 09:10:52 -08:00
Kohsuke Kawaguchi
d6722266f5 [maven-release-plugin] prepare release github-api-1.63 2015-03-02 09:10:49 -08:00
Kohsuke Kawaguchi
11566891dc Restored backward compatibility
The signature of the method can change for the future, but it still has
to return Label instances for older binaries
2015-03-02 08:33:00 -08:00
Kohsuke Kawaguchi
9aaf69cc9a Added maling list 2015-03-02 08:14:15 -08:00
Ashwanth Kumar
a716a59489 Picking endpoint from the properties file and environment variables
Helps seemless switching between public github and enterprise without any code changes
2015-02-22 09:41:58 +05:30
Daniel
bad0d1bbcf implementing github trees as described https://developer.github.com/v3/git/trees/#get-a-tree-recursively 2015-02-18 14:58:27 +01:00
Kohsuke Kawaguchi
aa43e265b7 [maven-release-plugin] prepare for next development iteration 2015-02-15 09:12:26 -08:00
Kohsuke Kawaguchi
67280951ff [maven-release-plugin] prepare release github-api-1.62 2015-02-15 09:12:23 -08:00
Kohsuke Kawaguchi
c3a9f6f9f5 Fixed NPE.
issue #152
2015-02-15 09:06:04 -08:00
Kohsuke Kawaguchi
e631e46dd1 Exposed this method.
I'm generally against having these inter-object short cut methods
(in this case it's getOwner().getName() but oh well.)

Fixes issue #149
2015-02-15 08:57:36 -08:00
Kohsuke Kawaguchi
15163ffde0 Avoid multiple concurrent population 2015-02-15 08:56:57 -08:00
Kohsuke Kawaguchi
b898284821 Mentions thread-safety and state the goal.
Most of the objects are effectively immutable, so this should be an easy goal

Fixes issue #148.
2015-02-15 08:56:29 -08:00
Kohsuke Kawaguchi
3bb7eb2e03 Added API to list contributors 2015-02-15 08:50:55 -08:00
Kohsuke Kawaguchi
11fcb9d456 Report the repository the push happened to
Fixes issue #144.
2015-02-15 08:42:32 -08:00
Kohsuke Kawaguchi
29f826448a Added a convenience method.
See: issue #134
2015-02-15 08:35:31 -08:00
Kohsuke Kawaguchi
a8cf4a7120 Added watch API support.
Fixes issue #130
2015-02-15 08:31:57 -08:00
Kohsuke Kawaguchi
60dce94a47 renamed to created RepositoryTest 2015-02-15 08:25:49 -08:00
Kohsuke Kawaguchi
c965b9cc24 follow up fix to the GHRepository.getApiTailUrl() change 2015-02-15 08:20:28 -08:00
Kohsuke Kawaguchi
762a32eb6d Added repository watch listing 2015-02-15 07:45:11 -08:00
Kohsuke Kawaguchi
541dac1aee Use getApiTailUrl for consistency 2015-02-15 07:35:02 -08:00
Kohsuke Kawaguchi
e2e2329301 Noting issue #60 that this method can return null 2015-02-15 07:16:18 -08:00
Kohsuke Kawaguchi
9afad71b0f Newly created user object resets root to null.
Fixes issue #111.
Test case from KostyaSha
2015-02-15 07:13:33 -08:00
Kohsuke Kawaguchi
7bbe0f7e8a Allow the client to explicitly control proxy
Fixes issue #109.
2015-02-15 07:02:50 -08:00
Kohsuke Kawaguchi
d90adfa98e Implemented label CRUD operations on GHRepository
Fixes issue #105
2015-02-15 06:55:35 -08:00
Kohsuke Kawaguchi
1dbcc4b776 Fixed the getReadme() method.
It was calling the wrong endpoint.
Fixed issue #99.
2015-02-15 06:31:22 -08:00
Kohsuke Kawaguchi
18696fca2d [maven-release-plugin] prepare for next development iteration 2015-02-14 10:28:55 -08:00
Kohsuke Kawaguchi
c40100b6da [maven-release-plugin] prepare release github-api-1.61 2015-02-14 10:28:51 -08:00
Kohsuke Kawaguchi
6396818740 MRELEASE-812 botched 1.60 2015-02-14 10:27:13 -08:00
53 changed files with 2512 additions and 773 deletions

14
pom.xml
View File

@@ -3,11 +3,11 @@
<parent> <parent>
<groupId>org.kohsuke</groupId> <groupId>org.kohsuke</groupId>
<artifactId>pom</artifactId> <artifactId>pom</artifactId>
<version>10</version> <version>14</version>
</parent> </parent>
<artifactId>github-api</artifactId> <artifactId>github-api</artifactId>
<version>1.60-SNAPSHOT</version> <version>1.67</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.67</tag>
</scm> </scm>
<distributionManagement> <distributionManagement>
@@ -139,4 +139,12 @@
<distribution>repo</distribution> <distribution>repo</distribution>
</license> </license>
</licenses> </licenses>
<mailingLists>
<mailingList>
<name>User List</name>
<post>github-api@googlegroups.com</post>
<archive>https://groups.google.com/forum/#!forum/github-api</archive>
</mailingList>
</mailingLists>
</project> </project>

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,78 +10,83 @@ 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";
public static final String USER_EMAIL = "user:email"; public static final String USER_EMAIL = "user:email";
public static final String USER_FOLLOW = "user:follow"; public static final String USER_FOLLOW = "user:follow";
public static final String PUBLIC_REPO = "public_repo"; public static final String PUBLIC_REPO = "public_repo";
public static final String REPO = "repo"; public static final String REPO = "repo";
public static final String REPO_STATUS = "repo:status"; public static final String REPO_STATUS = "repo:status";
public static final String DELETE_REPO = "delete_repo"; public static final String DELETE_REPO = "delete_repo";
public static final String NOTIFICATIONS = "notifications"; public static final String NOTIFICATIONS = "notifications";
public static final String GIST = "gist"; public static final String GIST = "gist";
public static final String READ_HOOK = "read:repo_hook"; public static final String READ_HOOK = "read:repo_hook";
public static final String WRITE_HOOK = "write:repo_hook"; public static final String WRITE_HOOK = "write:repo_hook";
public static final String AMIN_HOOK = "admin:repo_hook"; public static final String AMIN_HOOK = "admin:repo_hook";
public static final String READ_ORG = "read:org"; public static final String READ_ORG = "read:org";
public static final String WRITE_ORG = "write:org"; public static final String WRITE_ORG = "write:org";
public static final String ADMIN_ORG = "admin:org"; public static final String ADMIN_ORG = "admin:org";
public static final String READ_KEY = "read:public_key"; public static final String READ_KEY = "read:public_key";
public static final String WRITE_KEY = "write:public_key"; public static final String WRITE_KEY = "write:public_key";
public static final String ADMIN_KEY = "admin:public_key"; public static final String ADMIN_KEY = "admin:public_key";
private GitHub root; private GitHub root;
private List<String> scopes; private List<String> scopes;
private String token; private String token;
private App app; private App app;
private String note; private String note;
private String note_url; private String note_url;
public GitHub getRoot() { public GitHub getRoot() {
return root; return root;
} }
public List<String> getScopes() { public List<String> getScopes() {
return scopes; return scopes;
} }
public String getToken(){ public String getToken() {
return token; return token;
} }
public URL getAppUrl(){ public URL getAppUrl() {
return GitHub.parseURL(app.url); return GitHub.parseURL(app.url);
} }
public String getAppName() { public String getAppName() {
return app.name; return app.name;
} }
public URL getApiURL(){ public URL getApiURL() {
return GitHub.parseURL(url); return GitHub.parseURL(url);
} }
public String getNote() { /**
return note; * @deprecated This object has no HTML URL.
} */
@Override
public URL getHtmlUrl() {
return null;
}
public URL getNoteUrl(){ public String getNote() {
return note;
}
public URL getNoteUrl() {
return GitHub.parseURL(note_url); return GitHub.parseURL(note_url);
} }
/*package*/ GHAuthorization wrap(GitHub root) { /*package*/ GHAuthorization wrap(GitHub root) {
this.root = root; this.root = root;
return this; return this;
} }
private static class App {
private String url;
private String name;
private static class App{ }
private String url;
private String name;
}
} }

View File

@@ -32,26 +32,26 @@ public class GHCommit {
private int comment_count; private int comment_count;
@WithBridgeMethods(value=GHAuthor.class,castRequired=true) @WithBridgeMethods(value = GHAuthor.class, castRequired = true)
public GitUser getAuthor() { public GitUser getAuthor() {
return author; return author;
} }
@WithBridgeMethods(value=GHAuthor.class,castRequired=true) @WithBridgeMethods(value = GHAuthor.class, castRequired = true)
public GitUser getCommitter() { public GitUser getCommitter() {
return committer; return committer;
} }
/** /**
* Commit message. * Commit message.
*/ */
public String getMessage() { public String getMessage() {
return message; return message;
} }
public int getCommentCount() { public int getCommentCount() {
return comment_count; return comment_count;
} }
} }
/** /**
@@ -153,14 +153,13 @@ public class GHCommit {
Stats stats; Stats stats;
List<Parent> parents; List<Parent> parents;
User author,committer; User author,committer;
public ShortInfo getCommitShortInfo() { public ShortInfo getCommitShortInfo() {
return commit; return commit;
} }
/** /**
* The repository that contains the commit. * The repository that contains the commit.
*/ */
public GHRepository getOwner() { public GHRepository getOwner() {

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;
@@ -49,7 +50,15 @@ public class GHCommitStatus extends GHObject {
return creator; return creator;
} }
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

@@ -70,10 +70,10 @@ public class GHCompare {
} }
public GHCommit.File[] getFiles() { public GHCommit.File[] getFiles() {
return files; return files;
} }
public GHCompare wrap(GHRepository owner) { public GHCompare wrap(GHRepository owner) {
this.owner = owner; this.owner = owner;
for (Commit commit : commits) { for (Commit commit : commits) {
commit.wrapUp(owner); commit.wrapUp(owner);

View File

@@ -1,8 +1,10 @@
package org.kohsuke.github; package org.kohsuke.github;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.io.InputStream;
import java.util.List;
import javax.xml.bind.DatatypeConverter; import javax.xml.bind.DatatypeConverter;
@@ -14,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;
@@ -26,9 +34,10 @@ public class GHContent {
private String url; // this is the API url private String url; // this is the API url
private String git_url; // this is the Blob url private String git_url; // this is the Blob url
private String html_url; // this is the UI private String html_url; // this is the UI
private String download_url;
public GHRepository getOwner() { public GHRepository getOwner() {
return owner; return repository;
} }
public String getType() { public String getType() {
@@ -58,35 +67,34 @@ public class GHContent {
/** /**
* Retrieve the decoded content that is stored at this location. * Retrieve the decoded content that is stored at this location.
* *
* <p>
* Due to the nature of GitHub's API, you're not guaranteed that * Due to the nature of GitHub's API, you're not guaranteed that
* the content will already be populated, so this may trigger * the content will already be populated, so this may trigger
* network activity, and can throw an IOException. * network activity, and can throw an IOException.
**/ *
* @deprecated
* Use {@link #read()}
*/
public String getContent() throws IOException { public String getContent() throws IOException {
return new String(DatatypeConverter.parseBase64Binary(getEncodedContent())); 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 * Due to the nature of GitHub's API, you're not guaranteed that
* the content will already be populated, so this may trigger * the content will already be populated, so this may trigger
* network activity, and can throw an IOException. * network activity, and can throw an IOException.
**/ *
* @deprecated
* Use {@link #read()}
*/
public String getEncodedContent() throws IOException { public String getEncodedContent() throws IOException {
if (content != null) if (content!=null)
return content; return content;
else
GHContent retrievedContent = owner.getFileContent(path); return Base64.encodeBase64String(IOUtils.toByteArray(read()));
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;
} }
public String getUrl() { public String getUrl() {
@@ -101,6 +109,21 @@ public class GHContent {
return html_url; 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() { public boolean isFile() {
return "file".equals(type); return "file".equals(type);
} }
@@ -109,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.
*/ */
@@ -118,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);
} }
}; };
} }
@@ -143,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)
@@ -156,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;
@@ -168,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)
@@ -180,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

@@ -9,7 +9,7 @@ public class GHDeployKey {
protected String url, key, title; protected String url, key, title;
protected boolean verified; protected boolean verified;
protected int id; protected int id;
private GHRepository owner; private GHRepository owner;
public int getId() { public int getId() {
return id; return id;
@@ -31,10 +31,10 @@ public class GHDeployKey {
return verified; return verified;
} }
public GHDeployKey wrap(GHRepository repo) { public GHDeployKey wrap(GHRepository repo) {
this.owner = repo; this.owner = repo;
return this; return this;
} }
public String toString() { public String toString() {
return new ToStringBuilder(this).append("title",title).append("id",id).append("key",key).toString(); return new ToStringBuilder(this).append("title",title).append("id",id).append("key",key).toString();

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

@@ -120,6 +120,7 @@ public abstract class GHEventPayload {
private String ref; private String ref;
private int size; private int size;
private List<PushCommit> commits; private List<PushCommit> commits;
private GHRepository repository;
/** /**
* The SHA of the HEAD commit on the repository * The SHA of the HEAD commit on the repository
@@ -158,6 +159,17 @@ public abstract class GHEventPayload {
return commits; return commits;
} }
public GHRepository getRepository() {
return repository;
}
@Override
void wrapUp(GitHub root) {
super.wrapUp(root);
if (repository!=null)
repository.wrap(root);
}
/** /**
* Commit in a push * Commit in a push
*/ */

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;
@@ -15,6 +16,7 @@ import java.util.Map.Entry;
* @see GHUser#listGists() * @see GHUser#listGists()
* @see GitHub#getGist(String) * @see GitHub#getGist(String)
* @see GitHub#createGist() * @see GitHub#createGist()
* @see <a href="https://developer.github.com/v3/gists/">documentation</a>
*/ */
public class GHGist extends GHObject { public class GHGist extends GHObject {
/*package almost final*/ GHUser owner; /*package almost final*/ GHUser owner;
@@ -59,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

@@ -44,14 +44,15 @@ import java.util.Locale;
public class GHIssue extends GHObject { public class GHIssue extends GHObject {
GitHub root; GitHub root;
GHRepository owner; GHRepository owner;
// API v3 // API v3
protected GHUser assignee; protected GHUser assignee;
protected String state; protected String state;
protected int number; protected int number;
protected String closed_at; protected String closed_at;
protected int comments; protected int comments;
protected String body; protected String body;
// for backward compatibility with < 1.63, this collection needs to hold instances of Label, not GHLabel
protected List<Label> labels; protected List<Label> labels;
protected GHUser user; protected GHUser user;
protected String title, html_url; protected String title, html_url;
@@ -59,22 +60,10 @@ public class GHIssue extends GHObject {
protected GHMilestone milestone; protected GHMilestone milestone;
protected GHUser closed_by; protected GHUser closed_by;
public static class Label { /**
private String url; * @deprecated use {@link GHLabel}
private String name; */
private String color; public static class Label extends GHLabel {
public String getUrl() {
return url;
}
public String getName() {
return name;
}
public String getColor() {
return color;
}
} }
/*package*/ GHIssue wrap(GHRepository owner) { /*package*/ GHIssue wrap(GHRepository owner) {
@@ -85,9 +74,9 @@ public class GHIssue extends GHObject {
/*package*/ GHIssue wrap(GitHub root) { /*package*/ GHIssue wrap(GitHub root) {
this.root = root; this.root = root;
if(assignee != null) assignee.wrapUp(root); if(assignee != null) assignee.wrapUp(root);
if(user != null) user.wrapUp(root); if(user != null) user.wrapUp(root);
if(closed_by != null) closed_by.wrapUp(root); if(closed_by != null) closed_by.wrapUp(root);
return this; return this;
} }
@@ -134,20 +123,20 @@ public class GHIssue extends GHObject {
return Enum.valueOf(GHIssueState.class, state.toUpperCase(Locale.ENGLISH)); return Enum.valueOf(GHIssueState.class, state.toUpperCase(Locale.ENGLISH));
} }
public Collection<Label> getLabels() throws IOException { public Collection<GHLabel> getLabels() throws IOException {
if(labels == null){ if(labels == null){
return Collections.EMPTY_LIST; return Collections.emptyList();
} }
return Collections.unmodifiableList(labels); return Collections.<GHLabel>unmodifiableList(labels);
} }
public Date getClosedAt() { public Date getClosedAt() {
return GitHub.parseDate(closed_at); return GitHub.parseDate(closed_at);
} }
public URL getApiURL(){ public URL getApiURL(){
return GitHub.parseURL(url); return GitHub.parseURL(url);
} }
/** /**
* Updates the issue by adding a comment. * Updates the issue by adding a comment.
@@ -196,16 +185,16 @@ public class GHIssue extends GHObject {
/** /**
* Obtains all the comments associated with this issue. * Obtains all the comments associated with this issue.
* *
* @see #listComments() * @see #listComments()
*/
public List<GHIssueComment> getComments() throws IOException {
return listComments().asList();
}
/**
* Obtains all the comments associated with this issue.
*/ */
public List<GHIssueComment> getComments() throws IOException {
return listComments().asList();
}
/**
* Obtains all the comments associated with this issue.
*/
public PagedIterable<GHIssueComment> listComments() throws IOException { public PagedIterable<GHIssueComment> listComments() throws IOException {
return new PagedIterable<GHIssueComment>() { return new PagedIterable<GHIssueComment>() {
public PagedIterator<GHIssueComment> iterator() { public PagedIterator<GHIssueComment> iterator() {
@@ -227,50 +216,65 @@ public class GHIssue extends GHObject {
return "/repos/"+owner.getOwnerName()+"/"+owner.getName()+"/issues/"+number; return "/repos/"+owner.getOwnerName()+"/"+owner.getName()+"/issues/"+number;
} }
public GHUser getAssignee() { public GHUser getAssignee() {
return assignee; return assignee;
} }
/** /**
* User who submitted the issue. * User who submitted the issue.
*/ */
public GHUser getUser() { public GHUser getUser() {
return user; return user;
} }
public GHUser getClosedBy() {
if(!"closed".equals(state)) return null;
if(closed_by != null) return closed_by;
//TODO closed_by = owner.getIssue(number).getClosed_by();
return closed_by;
}
public int getCommentsCount(){
return comments;
}
public PullRequest getPullRequest() { /**
return pull_request; * Reports who has closed the issue.
} *
* <p>
* Note that GitHub doesn't always seem to report this information
* even for an issue that's already closed. See
* https://github.com/kohsuke/github-api/issues/60.
*/
public GHUser getClosedBy() {
if(!"closed".equals(state)) return null;
if(closed_by != null) return closed_by;
//TODO closed_by = owner.getIssue(number).getClosed_by();
return closed_by;
}
public int getCommentsCount(){
return comments;
}
public GHMilestone getMilestone() { /**
return milestone; * Returns non-null if this issue is a shadow of a pull request.
} */
public PullRequest getPullRequest() {
return pull_request;
}
public static class PullRequest{ public boolean isPullRequest() {
private String diff_url, patch_url, html_url; return pull_request!=null;
}
public URL getDiffUrl() {
return GitHub.parseURL(diff_url); public GHMilestone getMilestone() {
} return milestone;
}
public URL getPatchUrl() {
return GitHub.parseURL(patch_url); public static class PullRequest{
} private String diff_url, patch_url, html_url;
public URL getUrl() { public URL getDiffUrl() {
return GitHub.parseURL(html_url); return GitHub.parseURL(diff_url);
} }
}
public URL getPatchUrl() {
return GitHub.parseURL(patch_url);
}
public URL getUrl() {
return GitHub.parseURL(html_url);
}
}
} }

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

@@ -0,0 +1,37 @@
package org.kohsuke.github;
import java.io.IOException;
/**
* @author Kohsuke Kawaguchi
* @see GHIssue#getLabels()
* @see GHRepository#listLabels()
*/
public class GHLabel {
private String url, name, color;
private GHRepository repo;
public String getUrl() {
return url;
}
public String getName() {
return name;
}
/**
* Color code without leading '#', such as 'f29513'
*/
public String getColor() {
return color;
}
/*package*/ GHLabel wrapUp(GHRepository repo) {
this.repo = repo;
return this;
}
public void delete() throws IOException {
repo.root.retrieve().method("DELETE").to(url);
}
}

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;
@@ -11,52 +12,56 @@ import java.util.Locale;
*/ */
public class GHMilestone extends GHObject { public class GHMilestone extends GHObject {
GitHub root; GitHub root;
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() {
return root; return root;
} }
public GHRepository getOwner() { public GHRepository getOwner() {
return owner; return owner;
} }
public GHUser getCreator() { public GHUser getCreator() {
return creator; return creator;
} }
public Date getDueOn() { public Date getDueOn() {
if (due_on == null) return null; if (due_on == null) return null;
return GitHub.parseDate(due_on); return GitHub.parseDate(due_on);
} }
public String getTitle() { public String getTitle() {
return title; return title;
} }
public String getDescription() { public String getDescription() {
return description; return description;
} }
public int getClosedIssues() { public int getClosedIssues() {
return closed_issues; return closed_issues;
} }
public int getOpenIssues() { public int getOpenIssues() {
return open_issues; return open_issues;
} }
public int getNumber() { public int getNumber() {
return number; return number;
} }
public GHMilestoneState getState() { public URL getHtmlUrl() {
return Enum.valueOf(GHMilestoneState.class, state.toUpperCase(Locale.ENGLISH)); return GitHub.parseURL(html_url);
} }
public GHMilestoneState getState() {
return Enum.valueOf(GHMilestoneState.class, state.toUpperCase(Locale.ENGLISH));
}
/** /**
* Closes this issue. * Closes this issue.
@@ -73,9 +78,9 @@ public class GHMilestone extends GHObject {
return "/repos/"+owner.getOwnerName()+"/"+owner.getName()+"/milestones/"+number; return "/repos/"+owner.getOwnerName()+"/"+owner.getName()+"/milestones/"+number;
} }
public GHMilestone wrap(GHRepository repo) { public GHMilestone wrap(GHRepository repo) {
this.owner = repo; this.owner = repo;
this.root = repo.root; this.root = repo.root;
return this; return this;
} }
} }

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;
@@ -36,7 +37,7 @@ public abstract class GHPerson extends GHObject {
* *
* Depending on the original API call where this object is created, it may not contain everything. * Depending on the original API call where this object is created, it may not contain everything.
*/ */
protected void populate() throws IOException { protected synchronized void populate() throws IOException {
if (created_at!=null) return; // already populated if (created_at!=null) return; // already populated
root.retrieve().to(url, this); root.retrieve().to(url, this);
@@ -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

@@ -37,20 +37,20 @@ import java.util.Locale;
*/ */
@SuppressWarnings({"UnusedDeclaration"}) @SuppressWarnings({"UnusedDeclaration"})
public class GHPullRequest extends GHIssue { public class GHPullRequest extends GHIssue {
private String patch_url, diff_url, issue_url; private String patch_url, diff_url, issue_url;
private GHCommitPointer base; private GHCommitPointer base;
private String merged_at; private String merged_at;
private GHCommitPointer head; private GHCommitPointer head;
// details that are only available when obtained from ID // details that are only available when obtained from ID
private GHUser merged_by; private GHUser merged_by;
private int review_comments, additions; private int review_comments, additions;
private boolean merged; private boolean merged;
private Boolean mergeable; private Boolean mergeable;
private int deletions; private int deletions;
private String mergeable_state; private String mergeable_state;
private int changed_files; private int changed_files;
/** /**
* GitHub doesn't return some properties of {@link GHIssue} when requesting the GET on the 'pulls' API * GitHub doesn't return some properties of {@link GHIssue} when requesting the GET on the 'pulls' API
@@ -60,15 +60,15 @@ public class GHPullRequest extends GHIssue {
private transient boolean fetchedIssueDetails; private transient boolean fetchedIssueDetails;
GHPullRequest wrapUp(GHRepository owner) { GHPullRequest wrapUp(GHRepository owner) {
this.wrap(owner); this.wrap(owner);
return wrapUp(owner.root); return wrapUp(owner.root);
} }
GHPullRequest wrapUp(GitHub root) { GHPullRequest wrapUp(GitHub root) {
if (owner!=null) owner.wrap(root); if (owner != null) owner.wrap(root);
if (base!=null) base.wrapUp(root); if (base != null) base.wrapUp(root);
if (head!=null) head.wrapUp(root); if (head != null) head.wrapUp(root);
if (merged_by != null) merged_by.wrapUp(root); if (merged_by != null) merged_by.wrapUp(root);
return this; return this;
} }
@@ -85,8 +85,8 @@ public class GHPullRequest extends GHIssue {
public URL getPatchUrl() { public URL getPatchUrl() {
return GitHub.parseURL(patch_url); return GitHub.parseURL(patch_url);
} }
/** /**
* The URL of the patch file. * The URL of the patch file.
* like https://github.com/jenkinsci/jenkins/pull/100.patch * like https://github.com/jenkinsci/jenkins/pull/100.patch
*/ */
@@ -108,8 +108,8 @@ public class GHPullRequest extends GHIssue {
public GHCommitPointer getHead() { public GHCommitPointer getHead() {
return head; return head;
} }
@Deprecated @Deprecated
public Date getIssueUpdatedAt() throws IOException { public Date getIssueUpdatedAt() throws IOException {
return super.getUpdatedAt(); return super.getUpdatedAt();
} }
@@ -126,65 +126,65 @@ public class GHPullRequest extends GHIssue {
return GitHub.parseDate(merged_at); return GitHub.parseDate(merged_at);
} }
@Override @Override
public Collection<Label> getLabels() throws IOException { public Collection<GHLabel> getLabels() throws IOException {
fetchIssue(); fetchIssue();
return super.getLabels(); return super.getLabels();
} }
@Override @Override
public GHUser getClosedBy() { public GHUser getClosedBy() {
return null; return null;
} }
@Override @Override
public PullRequest getPullRequest() { public PullRequest getPullRequest() {
return null; return null;
} }
// //
// details that are only available via get with ID // details that are only available via get with ID
// //
// //
public GHUser getMergedBy() throws IOException { public GHUser getMergedBy() throws IOException {
populate(); populate();
return merged_by; return merged_by;
} }
public int getReviewComments() throws IOException { public int getReviewComments() throws IOException {
populate(); populate();
return review_comments; return review_comments;
} }
public int getAdditions() throws IOException { public int getAdditions() throws IOException {
populate(); populate();
return additions; return additions;
} }
public boolean isMerged() throws IOException { public boolean isMerged() throws IOException {
populate(); populate();
return merged; return merged;
} }
public Boolean getMergeable() throws IOException { public Boolean getMergeable() throws IOException {
populate(); populate();
return mergeable; return mergeable;
} }
public int getDeletions() throws IOException { public int getDeletions() throws IOException {
populate(); populate();
return deletions; return deletions;
} }
public String getMergeableState() throws IOException { public String getMergeableState() throws IOException {
populate(); populate();
return mergeable_state; return mergeable_state;
} }
public int getChangedFiles() throws IOException { public int getChangedFiles() throws IOException {
populate(); populate();
return changed_files; return changed_files;
} }
/** /**
* Fully populate the data by retrieving missing data. * Fully populate the data by retrieving missing data.
@@ -194,7 +194,7 @@ public class GHPullRequest extends GHIssue {
private void populate() throws IOException { private void populate() throws IOException {
if (merged_by!=null) return; // already populated if (merged_by!=null) return; // already populated
root.retrieve().to(url, this); root.retrieve().to(url, this).wrapUp(owner);
} }
/** /**

View File

@@ -1,5 +1,7 @@
package org.kohsuke.github; package org.kohsuke.github;
import java.util.Date;
/** /**
* Rate limit. * Rate limit.
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
@@ -10,12 +12,28 @@ public class GHRateLimit {
*/ */
public int remaining; public int remaining;
/** /**
* Alotted API call per hour. * Allotted API call per hour.
*/ */
public int limit; 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 @Override
public String toString() { public String toString() {
return remaining+"/"+limit; return "GHRateLimit{" +
"remaining=" + remaining +
", limit=" + limit +
", resetDate=" + getResetDate() +
'}';
} }
} }

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() {
@@ -120,7 +121,7 @@ public class GHRelease extends GHObject {
public GHAsset uploadAsset(File file, String contentType) throws IOException { public GHAsset uploadAsset(File file, String contentType) throws IOException {
Requester builder = new Requester(owner.root); Requester builder = new Requester(owner.root);
String url = format("https://uploads.github.com%sreleases/%d/assets?name=%s", String url = format("https://uploads.github.com%s/releases/%d/assets?name=%s",
owner.getApiTailUrl(""), getId(), file.getName()); owner.getApiTailUrl(""), getId(), file.getName());
return builder.contentType(contentType) return builder.contentType(contentType)
.with(new FileInputStream(file)) .with(new FileInputStream(file))

View File

@@ -30,7 +30,9 @@ import org.apache.commons.lang.StringUtils;
import javax.xml.bind.DatatypeConverter; import javax.xml.bind.DatatypeConverter;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.io.Reader;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
@@ -61,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);
} }
@@ -157,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
*/ */
@@ -195,7 +203,7 @@ public class GHRepository extends GHObject {
} }
public GHIssue getIssue(int id) throws IOException { public GHIssue getIssue(int id) throws IOException {
return root.retrieve().to("/repos/" + owner.login + "/" + name + "/issues/" + id, GHIssue.class).wrap(this); return root.retrieve().to(getApiTailUrl("issues/" + id), GHIssue.class).wrap(this);
} }
public GHIssueBuilder createIssue(String title) { public GHIssueBuilder createIssue(String title) {
@@ -208,8 +216,8 @@ public class GHRepository extends GHObject {
public List<GHIssue> getIssues(GHIssueState state, GHMilestone milestone) throws IOException { public List<GHIssue> getIssues(GHIssueState state, GHMilestone milestone) throws IOException {
return Arrays.asList(GHIssue.wrap(root.retrieve() return Arrays.asList(GHIssue.wrap(root.retrieve()
.to(String.format("/repos/%s/%s/issues?state=%s&milestone=%s", owner.login, name, .to(getApiTailUrl(String.format("issues?state=%s&milestone=%s",
state.toString().toLowerCase(), milestone == null ? "none" : "" + milestone.getNumber()), state.toString().toLowerCase(), milestone == null ? "none" : "" + milestone.getNumber())),
GHIssue[].class GHIssue[].class
), this)); ), this));
} }
@@ -285,7 +293,19 @@ public class GHRepository extends GHObject {
}; };
} }
protected String getOwnerName() { /**
* 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; return owner.login;
} }
@@ -339,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.
@@ -371,7 +391,7 @@ public class GHRepository extends GHObject {
return new PagedIterable<GHUser>() { return new PagedIterable<GHUser>() {
public PagedIterator<GHUser> iterator() { public PagedIterator<GHUser> iterator() {
return new PagedIterator<GHUser>(root.retrieve().asIterator("/repos/" + owner.login + "/" + name + "/collaborators", GHUser[].class)) { return new PagedIterator<GHUser>(root.retrieve().asIterator(getApiTailUrl("collaborators"), GHUser[].class)) {
@Override @Override
protected void wrapUp(GHUser[] users) { protected void wrapUp(GHUser[] users) {
@@ -392,7 +412,7 @@ public class GHRepository extends GHObject {
*/ */
public Set<String> getCollaboratorNames() throws IOException { public Set<String> getCollaboratorNames() throws IOException {
Set<String> r = new HashSet<String>(); Set<String> r = new HashSet<String>();
for (GHUser u : GHUser.wrap(root.retrieve().to("/repos/" + owner.login + "/" + name + "/collaborators", GHUser[].class),root)) for (GHUser u : GHUser.wrap(root.retrieve().to(getApiTailUrl("collaborators"), GHUser[].class),root))
r.add(u.login); r.add(u.login);
return r; return r;
} }
@@ -401,7 +421,7 @@ public class GHRepository extends GHObject {
* If this repository belongs to an organization, return a set of teams. * If this repository belongs to an organization, return a set of teams.
*/ */
public Set<GHTeam> getTeams() throws IOException { public Set<GHTeam> getTeams() throws IOException {
return Collections.unmodifiableSet(new HashSet<GHTeam>(Arrays.asList(GHTeam.wrapUp(root.retrieve().to("/repos/" + owner.login + "/" + name + "/teams", GHTeam[].class), root.getOrganization(owner.login))))); return Collections.unmodifiableSet(new HashSet<GHTeam>(Arrays.asList(GHTeam.wrapUp(root.retrieve().to(getApiTailUrl("teams"), GHTeam[].class), root.getOrganization(owner.login)))));
} }
public void addCollaborators(GHUser... users) throws IOException { public void addCollaborators(GHUser... users) throws IOException {
@@ -423,7 +443,7 @@ public class GHRepository extends GHObject {
private void modifyCollaborators(Collection<GHUser> users, String method) throws IOException { private void modifyCollaborators(Collection<GHUser> users, String method) throws IOException {
verifyMine(); verifyMine();
for (GHUser user : users) { for (GHUser user : users) {
new Requester(root).method(method).to("/repos/" + owner.login + "/" + name + "/collaborators/" + user.getLogin()); new Requester(root).method(method).to(getApiTailUrl("collaborators/" + user.getLogin()));
} }
} }
@@ -431,14 +451,14 @@ public class GHRepository extends GHObject {
Map<String, String> config = new HashMap<String, String>(); Map<String, String> config = new HashMap<String, String>();
config.put("address", address); config.put("address", address);
new Requester(root).method("POST").with("name", "email").with("config", config).with("active", "true") new Requester(root).method("POST").with("name", "email").with("config", config).with("active", "true")
.to(String.format("/repos/%s/%s/hooks", owner.login, name)); .to(getApiTailUrl("hooks"));
} }
private void edit(String key, String value) throws IOException { private void edit(String key, String value) throws IOException {
Requester requester = new Requester(root); Requester requester = new Requester(root);
if (!key.equals("name")) if (!key.equals("name"))
requester.with("name", name); // even when we don't change the name, we need to send it in requester.with("name", name); // even when we don't change the name, we need to send it in
requester.with(key, value).method("PATCH").to("/repos/" + owner.login + "/" + name); requester.with(key, value).method("PATCH").to(getApiTailUrl(""));
} }
/** /**
@@ -479,7 +499,7 @@ public class GHRepository extends GHObject {
*/ */
public void delete() throws IOException { public void delete() throws IOException {
try { try {
new Requester(root).method("DELETE").to("/repos/" + owner.login + "/" + name); new Requester(root).method("DELETE").to(getApiTailUrl(""));
} catch (FileNotFoundException x) { } catch (FileNotFoundException x) {
throw (FileNotFoundException) new FileNotFoundException("Failed to delete " + owner.login + "/" + name + "; might not exist, or you might need the delete_repo scope in your token: http://stackoverflow.com/a/19327004/12916").initCause(x); throw (FileNotFoundException) new FileNotFoundException("Failed to delete " + owner.login + "/" + name + "; might not exist, or you might need the delete_repo scope in your token: http://stackoverflow.com/a/19327004/12916").initCause(x);
} }
@@ -492,7 +512,7 @@ public class GHRepository extends GHObject {
* Newly forked repository that belong to you. * Newly forked repository that belong to you.
*/ */
public GHRepository fork() throws IOException { public GHRepository fork() throws IOException {
return new Requester(root).method("POST").to("/repos/" + owner.login + "/" + name + "/forks", GHRepository.class).wrap(root); return new Requester(root).method("POST").to(getApiTailUrl("forks"), GHRepository.class).wrap(root);
} }
/** /**
@@ -502,7 +522,7 @@ public class GHRepository extends GHObject {
* Newly forked repository that belong to you. * Newly forked repository that belong to you.
*/ */
public GHRepository forkTo(GHOrganization org) throws IOException { public GHRepository forkTo(GHOrganization org) throws IOException {
new Requester(root).to(String.format("/repos/%s/%s/forks?org=%s", owner.login, name, org.getLogin())); new Requester(root).to(getApiTailUrl("forks?org="+org.getLogin()));
// this API is asynchronous. we need to wait for a bit // this API is asynchronous. we need to wait for a bit
for (int i=0; i<10; i++) { for (int i=0; i<10; i++) {
@@ -521,7 +541,7 @@ public class GHRepository extends GHObject {
* Retrieves a specified pull request. * Retrieves a specified pull request.
*/ */
public GHPullRequest getPullRequest(int i) throws IOException { public GHPullRequest getPullRequest(int i) throws IOException {
return root.retrieve().to("/repos/" + owner.login + '/' + name + "/pulls/" + i, GHPullRequest.class).wrapUp(this); return root.retrieve().to(getApiTailUrl("pulls/" + i), GHPullRequest.class).wrapUp(this);
} }
/** /**
@@ -539,7 +559,7 @@ public class GHRepository extends GHObject {
public PagedIterable<GHPullRequest> listPullRequests(final GHIssueState state) { public PagedIterable<GHPullRequest> listPullRequests(final GHIssueState state) {
return new PagedIterable<GHPullRequest>() { return new PagedIterable<GHPullRequest>() {
public PagedIterator<GHPullRequest> iterator() { public PagedIterator<GHPullRequest> iterator() {
return new PagedIterator<GHPullRequest>(root.retrieve().asIterator(String.format("/repos/%s/%s/pulls?state=%s", owner.login, name, state.name().toLowerCase(Locale.ENGLISH)), GHPullRequest[].class)) { return new PagedIterator<GHPullRequest>(root.retrieve().asIterator(getApiTailUrl("pulls?state="+state.name().toLowerCase(Locale.ENGLISH)), GHPullRequest[].class)) {
@Override @Override
protected void wrapUp(GHPullRequest[] page) { protected void wrapUp(GHPullRequest[] page) {
for (GHPullRequest pr : page) for (GHPullRequest pr : page)
@@ -578,14 +598,14 @@ public class GHRepository extends GHObject {
*/ */
public List<GHHook> getHooks() throws IOException { public List<GHHook> getHooks() throws IOException {
List<GHHook> list = new ArrayList<GHHook>(Arrays.asList( List<GHHook> list = new ArrayList<GHHook>(Arrays.asList(
root.retrieve().to(String.format("/repos/%s/%s/hooks", owner.login, name), GHHook[].class))); root.retrieve().to(getApiTailUrl("hooks"), GHHook[].class)));
for (GHHook h : list) for (GHHook h : list)
h.wrap(this); h.wrap(this);
return list; return list;
} }
public GHHook getHook(int id) throws IOException { public GHHook getHook(int id) throws IOException {
return root.retrieve().to(String.format("/repos/%s/%s/hooks/%d", owner.login, name, id), GHHook.class).wrap(this); return root.retrieve().to(getApiTailUrl("hooks/" + id), GHHook.class).wrap(this);
} }
/** /**
@@ -597,7 +617,7 @@ public class GHRepository extends GHObject {
* @throws IOException on failure communicating with GitHub * @throws IOException on failure communicating with GitHub
*/ */
public GHCompare getCompare(String id1, String id2) throws IOException { public GHCompare getCompare(String id1, String id2) throws IOException {
GHCompare compare = root.retrieve().to(String.format("/repos/%s/%s/compare/%s...%s", owner.login, name, id1, id2), GHCompare.class); GHCompare compare = root.retrieve().to(getApiTailUrl(String.format("compare/%s...%s", id1, id2)), GHCompare.class);
return compare.wrap(this); return compare.wrap(this);
} }
@@ -628,18 +648,47 @@ public class GHRepository extends GHObject {
return GHRef.wrap(root.retrieve().to(String.format("/repos/%s/%s/git/refs/%s", owner.login, name, refType), GHRef[].class),root); return GHRef.wrap(root.retrieve().to(String.format("/repos/%s/%s/git/refs/%s", owner.login, name, refType), GHRef[].class),root);
} }
/** /**
* Retrive a ref of the given type for the current GitHub repository. * Retrive a ref of the given type for the current GitHub repository.
* *
* @param refName * @param refName
* eg: heads/branch * eg: heads/branch
* @return refs matching the request type * @return refs matching the request type
* @throws IOException * @throws IOException
* on failure communicating with GitHub, potentially due to an * on failure communicating with GitHub, potentially due to an
* invalid ref type being requested * invalid ref type being requested
*/ */
public GHRef getRef(String refName) throws IOException { public GHRef getRef(String refName) throws IOException {
return root.retrieve().to(String.format("/repos/%s/%s/git/refs/%s", owner.login, name, refName), GHRef.class).wrap(root); 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. * Gets a commit object in this repository.
*/ */
@@ -737,10 +786,10 @@ 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);
} }
/** /**
@@ -760,6 +809,54 @@ public class GHRepository extends GHObject {
}; };
} }
/**
* Lists labels in this repository.
*
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
*/
public PagedIterable<GHLabel> listLabels() throws IOException {
return new PagedIterable<GHLabel>() {
public PagedIterator<GHLabel> iterator() {
return new PagedIterator<GHLabel>(root.retrieve().asIterator(getApiTailUrl("labels"), GHLabel[].class)) {
@Override
protected void wrapUp(GHLabel[] page) {
for (GHLabel c : page)
c.wrapUp(GHRepository.this);
}
};
}
};
}
public GHLabel getLabel(String name) throws IOException {
return root.retrieve().to(getApiTailUrl("labels/"+name), GHLabel.class).wrapUp(this);
}
public GHLabel createLabel(String name, String color) throws IOException {
return root.retrieve().method("POST")
.with("name",name)
.with("color",color)
.to(getApiTailUrl("labels"), GHLabel.class).wrapUp(this);
}
/**
* Lists all the subscribers (aka watchers.)
*
* https://developer.github.com/v3/activity/watching/
*/
public PagedIterable<GHUser> listSubscribers() {
return new PagedIterable<GHUser>() {
public PagedIterator<GHUser> iterator() {
return new PagedIterator<GHUser>(root.retrieve().asIterator(getApiTailUrl("subscribers"), GHUser[].class)) {
protected void wrapUp(GHUser[] page) {
for (GHUser c : page)
c.wrapUp(root);
}
};
}
};
}
/** /**
* *
* See https://api.github.com/hooks for possible names and their configuration scheme. * See https://api.github.com/hooks for possible names and their configuration scheme.
@@ -898,10 +995,10 @@ public class GHRepository extends GHObject {
*/ */
public Map<Integer, GHMilestone> getMilestones() throws IOException { public Map<Integer, GHMilestone> getMilestones() throws IOException {
Map<Integer,GHMilestone> milestones = new TreeMap<Integer, GHMilestone>(); Map<Integer,GHMilestone> milestones = new TreeMap<Integer, GHMilestone>();
for (GHMilestone m : listMilestones(GHIssueState.OPEN)) { for (GHMilestone m : listMilestones(GHIssueState.OPEN)) {
milestones.put(m.getNumber(), m); milestones.put(m.getNumber(), m);
} }
return milestones; return milestones;
} }
/** /**
@@ -921,16 +1018,16 @@ public class GHRepository extends GHObject {
}; };
} }
public GHMilestone getMilestone(int number) throws IOException { public GHMilestone getMilestone(int number) throws IOException {
GHMilestone m = milestones.get(number); GHMilestone m = milestones.get(number);
if (m == null) { if (m == null) {
m = root.retrieve().to(getApiTailUrl("milestones/" + number), GHMilestone.class); m = root.retrieve().to(getApiTailUrl("milestones/" + number), GHMilestone.class);
m.owner = this; m.owner = this;
m.root = root; m.root = root;
milestones.put(m.getNumber(), m); milestones.put(m.getNumber(), m);
} }
return m; return m;
} }
public GHContent getFileContent(String path) throws IOException { public GHContent getFileContent(String path) throws IOException {
return getFileContent(path, null); return getFileContent(path, null);
@@ -938,12 +1035,9 @@ public class GHRepository extends GHObject {
public GHContent getFileContent(String path, String ref) throws IOException { public GHContent getFileContent(String path, String ref) throws IOException {
Requester requester = root.retrieve(); Requester requester = root.retrieve();
String target = String.format("/repos/%s/%s/contents/%s", owner.login, name, path); String target = getApiTailUrl("contents/" + path);
if (ref != null) return requester.with("ref",ref).to(target, GHContent.class).wrap(this);
target = target + "?ref=" + ref;
return requester.to(target, GHContent.class).wrap(this);
} }
public List<GHContent> getDirectoryContent(String path) throws IOException { public List<GHContent> getDirectoryContent(String path) throws IOException {
@@ -952,20 +1046,21 @@ public class GHRepository extends GHObject {
public List<GHContent> getDirectoryContent(String path, String ref) throws IOException { public List<GHContent> getDirectoryContent(String path, String ref) throws IOException {
Requester requester = root.retrieve(); Requester requester = root.retrieve();
String target = String.format("/repos/%s/%s/contents/%s", owner.login, name, path); String target = getApiTailUrl("contents/" + path);
if (ref != null) GHContent[] files = requester.with("ref",ref).to(target, GHContent[].class);
target = target + "?ref=" + ref;
GHContent[] files = requester.to(target, GHContent[].class);
GHContent.wrap(files, this); GHContent.wrap(files, this);
return Arrays.asList(files); return Arrays.asList(files);
} }
public GHContent getReadme() throws Exception { /**
return getFileContent("readme"); * https://developer.github.com/v3/repos/contents/#get-the-readme
*/
public GHContent getReadme() throws IOException {
Requester requester = root.retrieve();
return requester.to(getApiTailUrl("readme"), GHContent.class).wrap(this);
} }
public GHContentUpdateResponse createContent(String content, String commitMessage, String path) throws IOException { public GHContentUpdateResponse createContent(String content, String commitMessage, String path) throws IOException {
@@ -999,26 +1094,127 @@ public class GHRepository extends GHObject {
return response; return response;
} }
public GHMilestone createMilestone(String title, String description) throws IOException { public GHMilestone createMilestone(String title, String description) throws IOException {
return new Requester(root) return new Requester(root)
.with("title", title).with("description", description).method("POST").to(getApiTailUrl("milestones"), GHMilestone.class).wrap(this); .with("title", title).with("description", description).method("POST").to(getApiTailUrl("milestones"), GHMilestone.class).wrap(this);
} }
public GHDeployKey addDeployKey(String title,String key) throws IOException { public GHDeployKey addDeployKey(String title,String key) throws IOException {
return new Requester(root) return new Requester(root)
.with("title", title).with("key", key).method("POST").to(getApiTailUrl("keys"), GHDeployKey.class).wrap(this); .with("title", title).with("key", key).method("POST").to(getApiTailUrl("keys"), GHDeployKey.class).wrap(this);
} }
public List<GHDeployKey> getDeployKeys() throws IOException{ public List<GHDeployKey> getDeployKeys() throws IOException{
List<GHDeployKey> list = new ArrayList<GHDeployKey>(Arrays.asList( List<GHDeployKey> list = new ArrayList<GHDeployKey>(Arrays.asList(
root.retrieve().to(String.format("/repos/%s/%s/keys", owner.login, name), GHDeployKey[].class))); root.retrieve().to(getApiTailUrl("keys"), GHDeployKey[].class)));
for (GHDeployKey h : list) for (GHDeployKey h : list)
h.wrap(this); h.wrap(this);
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.
*/
public GHSubscription subscribe(boolean subscribed, boolean ignored) throws IOException {
return new Requester(root)
.with("subscribed", subscribed)
.with("ignored", ignored)
.method("PUT").to(getApiTailUrl("subscription"), GHSubscription.class).wrapUp(this);
}
/**
* Returns the current subscription.
*
* @return null if no subscription exists.
*/
public GHSubscription getSubscription() throws IOException {
try {
return new Requester(root).to(getApiTailUrl("subscription"), GHSubscription.class).wrapUp(this);
} catch (FileNotFoundException e) {
return null;
}
}
public PagedIterable<Contributor> listContributors() throws IOException {
return new PagedIterable<Contributor>() {
public PagedIterator<Contributor> iterator() {
return new PagedIterator<Contributor>(root.retrieve().asIterator(getApiTailUrl("contributors"), Contributor[].class)) {
@Override
protected void wrapUp(Contributor[] page) {
for (Contributor c : page)
c.wrapUp(root);
}
};
}
};
}
public static class Contributor extends GHUser {
private int contributions;
public int getContributions() {
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 @Override
public String toString() { public String toString() {
@@ -1041,6 +1237,7 @@ public class GHRepository extends GHObject {
} }
String getApiTailUrl(String tail) { String getApiTailUrl(String tail) {
return "/repos/" + owner.login + "/" + name +'/'+tail; if (tail.length()>0 && !tail.startsWith("/")) tail='/'+tail;
return "/repos/" + owner.login + "/" + name +tail;
} }
} }

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

@@ -0,0 +1,64 @@
package org.kohsuke.github;
import java.io.IOException;
import java.util.Date;
/**
* 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;
private boolean subscribed, ignored;
private GitHub root;
private GHRepository repo;
public Date getCreatedAt() {
return GitHub.parseDate(created_at);
}
public String getUrl() {
return url;
}
public String getRepositoryUrl() {
return repository_url;
}
public String getReason() {
return reason;
}
public boolean isSubscribed() {
return subscribed;
}
public boolean isIgnored() {
return ignored;
}
public GHRepository getRepository() {
return repo;
}
/**
* Removes this subscription.
*/
public void delete() throws IOException {
new Requester(root).method("DELETE").to(url);
}
GHSubscription wrapUp(GHRepository repo) {
this.repo = repo;
return wrapUp(repo.root);
}
GHSubscription wrapUp(GitHub root) {
this.root = root;
return this;
}
}

View File

@@ -15,6 +15,8 @@ public class GHTag {
/*package*/ GHTag wrap(GHRepository owner) { /*package*/ GHTag wrap(GHRepository owner) {
this.owner = owner; this.owner = owner;
this.root = owner.root; this.root = owner.root;
if (commit!=null)
commit.wrapUp(owner);
return this; return this;
} }

View File

@@ -47,10 +47,10 @@ public class GHTeam {
return id; return id;
} }
/** /**
* Retrieves the current members. * Retrieves the current members.
*/ */
public PagedIterable<GHUser> listMembers() throws IOException { public PagedIterable<GHUser> listMembers() throws IOException {
return new PagedIterable<GHUser>() { return new PagedIterable<GHUser>() {
public PagedIterator<GHUser> iterator() { public PagedIterator<GHUser> iterator() {
return new PagedIterator<GHUser>(org.root.retrieve().asIterator(api("/members"), GHUser[].class)) { return new PagedIterator<GHUser>(org.root.retrieve().asIterator(api("/members"), GHUser[].class)) {
@@ -64,8 +64,8 @@ public class GHTeam {
} }
public Set<GHUser> getMembers() throws IOException { public Set<GHUser> getMembers() throws IOException {
return Collections.unmodifiableSet(listMembers().asSet()); return Collections.unmodifiableSet(listMembers().asSet());
} }
/** /**
* Checks if this team has the specified user as a member. * Checks if this team has the specified user as a member.

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,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;
}
}

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

View File

@@ -69,6 +69,24 @@ public class GHUser extends GHPerson {
return new GHPersonSet<GHUser>(Arrays.asList(wrap(followers,root))); return new GHPersonSet<GHUser>(Arrays.asList(wrap(followers,root)));
} }
/**
* Lists all the subscribed (aka watched) repositories.
*
* https://developer.github.com/v3/activity/watching/
*/
public PagedIterable<GHRepository> listSubscriptions() {
return new PagedIterable<GHRepository>() {
public PagedIterator<GHRepository> iterator() {
return new PagedIterator<GHRepository>(root.retrieve().asIterator(getApiTailUrl("subscriptions"), GHRepository[].class)) {
protected void wrapUp(GHRepository[] page) {
for (GHRepository c : page)
c.wrap(root);
}
};
}
};
}
/** /**
* Returns true if this user belongs to the specified organization. * Returns true if this user belongs to the specified organization.
*/ */
@@ -162,4 +180,9 @@ public class GHUser extends GHPerson {
} }
return false; return false;
} }
String getApiTailUrl(String tail) {
if (tail.length()>0 && !tail.startsWith("/")) tail='/'+tail;
return "/users/" + login + tail;
}
} }

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

@@ -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.ANY;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
@@ -38,6 +40,7 @@ import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Hashtable;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -53,6 +56,12 @@ import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
/** /**
* Root of the GitHub API. * Root of the GitHub API.
* *
* <h2>Thread safety</h2>
* <p>
* This library aims to be safe for use by multiple threads concurrently, although
* the library itself makes no attempt to control/serialize potentially conflicting
* operations to GitHub, such as updating &amp; deleting a repository at the same time.
*
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
public class GitHub { public class GitHub {
@@ -63,11 +72,13 @@ public class GitHub {
*/ */
/*package*/ final String encodedAuthorization; /*package*/ final String encodedAuthorization;
private final Map<String,GHUser> users = new HashMap<String, GHUser>(); private final Map<String,GHUser> users = new Hashtable<String, GHUser>();
private final Map<String,GHOrganization> orgs = new HashMap<String, GHOrganization>(); private final Map<String,GHOrganization> orgs = new Hashtable<String, GHOrganization>();
private final String apiUrl; private final String apiUrl;
/*package*/ final RateLimitHandler rateLimitHandler;
private HttpConnector connector = HttpConnector.DEFAULT; private HttpConnector connector = HttpConnector.DEFAULT;
/** /**
@@ -106,7 +117,7 @@ public class GitHub {
* @param connector * @param connector
* HttpConnector to use. Pass null to use default connector. * HttpConnector to use. Pass null to use default connector.
*/ */
/* package */ GitHub(String apiUrl, String login, String oauthAccessToken, String password, HttpConnector connector) 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 if (apiUrl.endsWith("/")) apiUrl = apiUrl.substring(0, apiUrl.length()-1); // normalize
this.apiUrl = apiUrl; this.apiUrl = apiUrl;
if (null != connector) this.connector = connector; if (null != connector) this.connector = connector;
@@ -125,6 +136,7 @@ public class GitHub {
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;
} }
/** /**
@@ -240,11 +252,11 @@ public class GitHub {
} }
/** /**
* Gets the {@link GHUser} that represents yourself. * Gets the {@link GHUser} that represents yourself.
*/ */
@WithBridgeMethods(GHUser.class) @WithBridgeMethods(GHUser.class)
public GHMyself getMyself() throws IOException { public GHMyself getMyself() throws IOException {
requireCredential(); requireCredential();
GHMyself u = retrieve().to("/user", GHMyself.class); GHMyself u = retrieve().to("/user", GHMyself.class);
@@ -252,20 +264,20 @@ public class GitHub {
users.put(u.getLogin(), u); users.put(u.getLogin(), u);
return u; return u;
} }
/** /**
* Obtains the object that represents the named user. * Obtains the object that represents the named user.
*/ */
public GHUser getUser(String login) throws IOException { public GHUser getUser(String login) throws IOException {
GHUser u = users.get(login); GHUser u = users.get(login);
if (u == null) { if (u == null) {
u = retrieve().to("/users/" + login, GHUser.class); u = retrieve().to("/users/" + login, GHUser.class);
u.root = this; u.root = this;
users.put(u.getLogin(), u); users.put(u.getLogin(), u);
} }
return u; return u;
} }
/** /**
@@ -283,7 +295,7 @@ public class GitHub {
GHUser u = users.get(orig.getLogin()); GHUser u = users.get(orig.getLogin());
if (u==null) { if (u==null) {
orig.root = this; orig.root = this;
users.put(login,orig); users.put(orig.getLogin(),orig);
return orig; return orig;
} }
return u; return u;
@@ -324,27 +336,27 @@ public class GitHub {
return r; return r;
} }
/** /**
* Gets complete map of organizations/teams that current user belongs to. * Gets complete map of organizations/teams that current user belongs to.
* *
* Leverages the new GitHub API /user/teams made available recently 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 * get in a single call the complete set of organizations, teams and permissions
* in a single call. * in a single call.
*/ */
public Map<String, Set<GHTeam>> getMyTeams() throws IOException { public Map<String, Set<GHTeam>> getMyTeams() throws IOException {
Map<String, Set<GHTeam>> allMyTeams = new HashMap<String, Set<GHTeam>>(); Map<String, Set<GHTeam>> allMyTeams = new HashMap<String, Set<GHTeam>>();
for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) { for (GHTeam team : retrieve().to("/user/teams", GHTeam[].class)) {
team.wrapUp(this); team.wrapUp(this);
String orgLogin = team.getOrganization().getLogin(); String orgLogin = team.getOrganization().getLogin();
Set<GHTeam> teamsPerOrg = allMyTeams.get(orgLogin); Set<GHTeam> teamsPerOrg = allMyTeams.get(orgLogin);
if (teamsPerOrg == null) { if (teamsPerOrg == null) {
teamsPerOrg = new HashSet<GHTeam>(); teamsPerOrg = new HashSet<GHTeam>();
} }
teamsPerOrg.add(team); teamsPerOrg.add(team);
allMyTeams.put(orgLogin, teamsPerOrg); allMyTeams.put(orgLogin, teamsPerOrg);
}
return allMyTeams;
} }
return allMyTeams;
}
/** /**
* Public events visible to you. Equivalent of what's displayed on https://github.com/ * Public events visible to you. Equivalent of what's displayed on https://github.com/
@@ -403,14 +415,14 @@ public class GitHub {
* *
* @see <a href="http://developer.github.com/v3/oauth/#create-a-new-authorization">Documentation</a> * @see <a href="http://developer.github.com/v3/oauth/#create-a-new-authorization">Documentation</a>
*/ */
public GHAuthorization createToken(Collection<String> scope, String note, String noteUrl) throws IOException{ public GHAuthorization createToken(Collection<String> scope, String note, String noteUrl) throws IOException{
Requester requester = new Requester(this) Requester requester = new Requester(this)
.with("scopes", scope) .with("scopes", scope)
.with("note", note) .with("note", note)
.with("note_url", noteUrl); .with("note_url", noteUrl);
return requester.method("POST").to("/authorizations", GHAuthorization.class).wrap(this); return requester.method("POST").to("/authorizations", GHAuthorization.class).wrap(this);
} }
/** /**
* Ensures that the credential is valid. * Ensures that the credential is valid.
@@ -431,6 +443,81 @@ 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.
* @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 youve 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) { /*package*/ static URL parseURL(String s) {
try { try {

View File

@@ -6,102 +6,122 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Properties; import java.util.Properties;
/** /**
*
*
* @since 1.59 * @since 1.59
*/ */
public class GitHubBuilder { public class GitHubBuilder {
private String endpoint = GitHub.GITHUB_URL;
// default scoped so unit tests can read them. // default scoped so unit tests can read them.
/* private */ String endpoint = GitHub.GITHUB_URL;
/* private */ String user; /* private */ String user;
/* private */ String password; /* private */ String password;
/* private */ String oauthToken; /* private */ String oauthToken;
private HttpConnector connector; private HttpConnector connector;
private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
public GitHubBuilder() { public GitHubBuilder() {
} }
/** /**
* First check if the credentials are configured using the ~/.github properties file. * 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 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. * 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. * @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. * @throws IOException If there are no credentials defined in the ~/.github properties file or the process environment.
*/ */
public static GitHubBuilder fromCredentials() throws IOException { public static GitHubBuilder fromCredentials() throws IOException {
Exception cause = null;
GitHubBuilder builder; GitHubBuilder builder;
try {
builder = fromPropertyFile(); try {
builder = fromPropertyFile();
if (builder.user != null)
return builder; if (builder.user != null)
else { return builder;
} catch (FileNotFoundException e) {
// this is the case where the ~/.github file exists but has no content. // fall through
cause = e;
builder = fromEnvironment(); }
if (builder.user != null) builder = fromEnvironment();
return builder;
else if (builder.user != null)
throw new IOException("Failed to resolve credentials from ~/.github or the environment."); return builder;
} else
throw (IOException)new IOException("Failed to resolve credentials from ~/.github or the environment.").initCause(cause);
} 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);
}
} }
/**
* @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 { public static GitHubBuilder fromEnvironment(String loginVariableName, String passwordVariableName, String oauthVariableName) throws IOException {
return fromEnvironment(loginVariableName, passwordVariableName, oauthVariableName, "");
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);
} }
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 { public static GitHubBuilder fromEnvironment() throws IOException {
Properties props = new Properties();
Properties props = new Properties(); for (Entry<String, String> e : System.getenv().entrySet()) {
String name = e.getKey().toLowerCase(Locale.ENGLISH);
Map<String, String> env = System.getenv(); if (name.startsWith("github_")) name=name.substring(7);
props.put(name,e.getValue());
for (Map.Entry<String, String> element : env.entrySet()) { }
props.put(element.getKey(), element.getValue());
}
return fromProperties(props); return fromProperties(props);
} }
@@ -110,7 +130,7 @@ public class GitHubBuilder {
File propertyFile = new File(homeDir, ".github"); File propertyFile = new File(homeDir, ".github");
return fromPropertyFile(propertyFile.getPath()); return fromPropertyFile(propertyFile.getPath());
} }
public static GitHubBuilder fromPropertyFile(String propertyFileName) throws IOException { public static GitHubBuilder fromPropertyFile(String propertyFileName) throws IOException {
Properties props = new Properties(); Properties props = new Properties();
FileInputStream in = null; FileInputStream in = null;
@@ -128,6 +148,7 @@ public class GitHubBuilder {
GitHubBuilder self = new GitHubBuilder(); GitHubBuilder self = new GitHubBuilder();
self.withOAuthToken(props.getProperty("oauth"), props.getProperty("login")); self.withOAuthToken(props.getProperty("oauth"), props.getProperty("login"));
self.withPassword(props.getProperty("login"), props.getProperty("password")); self.withPassword(props.getProperty("login"), props.getProperty("password"));
self.withEndpoint(props.getProperty("endpoint", GitHub.GITHUB_URL));
return self; return self;
} }
@@ -152,8 +173,25 @@ public class GitHubBuilder {
this.connector = connector; this.connector = connector;
return this; return this;
} }
public GitHubBuilder withRateLimitHandler(RateLimitHandler handler) {
this.rateLimitHandler = handler;
return this;
}
/**
* Configures {@linkplain #withConnector(HttpConnector) connector}
* that uses HTTP library in JRE but use a specific proxy, instead of
* the system default one.
*/
public GitHubBuilder withProxy(final Proxy p) {
return withConnector(new HttpConnector() {
public HttpURLConnection connect(URL url) throws IOException {
return (HttpURLConnection) url.openConnection(p);
}
});
}
public GitHub build() throws IOException { public GitHub build() throws IOException {
return new GitHub(endpoint, user, oauthToken, password, connector); return new GitHub(endpoint, user, oauthToken, password, connector, rateLimitHandler);
} }
} }

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

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

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

View File

@@ -23,13 +23,13 @@
*/ */
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;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.Reader; import java.io.Reader;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array; import java.lang.reflect.Array;
@@ -44,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;
@@ -52,9 +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 org.apache.commons.io.IOUtils; import static org.kohsuke.github.GitHub.*;
import javax.net.ssl.HttpsURLConnection;
/** /**
* A builder pattern for making HTTP call and parsing its output. * A builder pattern for making HTTP call and parsing its output.
@@ -64,6 +63,7 @@ import javax.net.ssl.HttpsURLConnection;
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.
@@ -72,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;
@@ -86,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.
*/ */
@@ -109,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);
@@ -190,12 +208,20 @@ class Requester {
private <T> T _to(String tailApiUrl, Class<T> type, T instance) throws IOException { private <T> T _to(String tailApiUrl, Class<T> type, T instance) throws IOException {
while (true) {// loop while API rate limit is hit 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 { 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\"")) {
@@ -216,7 +242,7 @@ class Requester {
} }
return result; return result;
} catch (IOException e) { } catch (IOException e) {
handleApiError(e,uc); handleApiError(e);
} }
} }
} }
@@ -226,19 +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);
} }
} }
} }
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")) { if (!method.equals("GET")) {
uc.setDoOutput(true); uc.setDoOutput(true);
uc.setRequestProperty("Content-type", contentType); uc.setRequestProperty("Content-type", contentType);
@@ -327,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) {
@@ -345,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;
@@ -366,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) {
@@ -387,16 +441,21 @@ 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)
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) if (instance!=null)
return MAPPER.readerForUpdating(instance).<T>readValue(data); return MAPPER.readerForUpdating(instance).<T>readValue(data);
return null; return null;
@@ -408,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);
@@ -417,28 +476,25 @@ class Requester {
} }
/** /**
* If the error is because of the API limit, wait 10 sec and return normally. * Handle API error by either throwing it or by returning normally to retry.
* Otherwise throw an exception reporting an error.
*/ */
/*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"))) {
// API limit reached. wait 10 secs and return normally root.rateLimitHandler.onError(e,uc);
try {
Thread.sleep(10000);
return;
} catch (InterruptedException _) {
throw (InterruptedIOException)new InterruptedIOException().initCause(e);
}
} }
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) {
throw (IOException)new IOException(IOUtils.toString(es,"UTF-8")).initCause(e); if (e instanceof FileNotFoundException) {
else // 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; 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

@@ -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. The library supports both github.com and GitHub Enterprise.
There are some corners of the GitHub API that's not yet implemented, but Most of the GitHub APIs are covered, although there are some corners that are still not yet implemented.
the library is implemented with the right abstractions and libraries to make it very easy to improve the coverage.
Sample Usage 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/), 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. 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

@@ -15,6 +15,7 @@
<item name="Introduction" href="/index.html"/> <item name="Introduction" href="/index.html"/>
<item name="Download" href="http://mvnrepository.com/artifact/${project.groupId}/${project.artifactId}"/> <item name="Download" href="http://mvnrepository.com/artifact/${project.groupId}/${project.artifactId}"/>
<item name="Source code" href="https://github.com/kohsuke/${project.artifactId}"/> <item name="Source code" href="https://github.com/kohsuke/${project.artifactId}"/>
<item name="Mailing List" href="https://groups.google.com/forum/#!forum/github-api"/>
</menu> </menu>
<menu name="References"> <menu name="References">

View File

@@ -1,22 +1,17 @@
import org.kohsuke.github.GHIssue; import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHTeam;
import org.kohsuke.github.GitHub; import org.kohsuke.github.GitHub;
import org.kohsuke.github.PagedIterable;
import org.kohsuke.github.PagedSearchIterable;
import java.util.Arrays; import java.util.Collection;
import java.util.Map;
/** /**
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
*/ */
public class Foo { public class Foo {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
PagedSearchIterable<GHIssue> reviewbybees = GitHub.connect().searchIssues().mentions("reviewbybees").isOpen().list(); Collection<GHRepository> lst = GitHub.connect().getUser("kohsuke").getRepositories().values();
for (GHIssue r : reviewbybees) { for (GHRepository r : lst) {
System.out.println(r.getTitle()); System.out.println(r.getName());
} }
System.out.println("total="+reviewbybees.getTotalCount()); System.out.println(lst.size());
} }
} }

View File

@@ -1,12 +1,10 @@
package org.kohsuke.github; package org.kohsuke.github;
import org.apache.commons.io.IOUtils;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.kohsuke.randname.RandomNameGenerator; import org.kohsuke.randname.RandomNameGenerator;
import java.io.FileInputStream; import java.io.File;
import java.util.Properties;
/** /**
* @author Kohsuke Kawaguchi * @author Kohsuke Kawaguchi
@@ -17,18 +15,13 @@ public abstract class AbstractGitHubApiTestBase extends Assert {
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
Properties props = new Properties(); File f = new File(System.getProperty("user.home"), ".github.kohsuke2");
java.io.File f = new java.io.File(System.getProperty("user.home"), ".github.kohsuke2");
if (f.exists()) { if (f.exists()) {
FileInputStream in = new FileInputStream(f); // use the non-standard credential preferentially, so that developers of this library do not have
try { // to clutter their event stream.
props.load(in); gitHub = GitHubBuilder.fromPropertyFile(f.getPath()).withRateLimitHandler(RateLimitHandler.FAIL).build();
gitHub = GitHub.connect(props.getProperty("login"),props.getProperty("oauth"));
} finally {
IOUtils.closeQuietly(in);
}
} else { } else {
gitHub = GitHub.connect(); gitHub = GitHubBuilder.fromCredentials().withRateLimitHandler(RateLimitHandler.FAIL).build();
} }
} }

View File

@@ -3,6 +3,8 @@ package org.kohsuke.github;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Test; import org.junit.Test;
import org.kohsuke.github.GHCommit.File; import org.kohsuke.github.GHCommit.File;
@@ -12,6 +14,7 @@ import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.regex.Pattern;
/** /**
* Unit test for simple App. * Unit test for simple App.
@@ -204,31 +207,31 @@ public class AppTest extends AbstractGitHubApiTestBase {
@Test @Test
public void testMyTeamsContainsAllMyOrganizations() throws IOException { public void testMyTeamsContainsAllMyOrganizations() throws IOException {
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams(); Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
Map<String, GHOrganization> myOrganizations = gitHub.getMyOrganizations(); Map<String, GHOrganization> myOrganizations = gitHub.getMyOrganizations();
assertEquals(teams.keySet(), myOrganizations.keySet()); assertEquals(teams.keySet(), myOrganizations.keySet());
} }
@Test @Test
public void testMyTeamsShouldIncludeMyself() throws IOException { public void testMyTeamsShouldIncludeMyself() throws IOException {
Map<String, Set<GHTeam>> teams = gitHub.getMyTeams(); Map<String, Set<GHTeam>> teams = gitHub.getMyTeams();
for (Entry<String, Set<GHTeam>> teamsPerOrg : teams.entrySet()) { for (Entry<String, Set<GHTeam>> teamsPerOrg : teams.entrySet()) {
String organizationName = teamsPerOrg.getKey(); String organizationName = teamsPerOrg.getKey();
for (GHTeam team : teamsPerOrg.getValue()) { for (GHTeam team : teamsPerOrg.getValue()) {
String teamName = team.getName(); String teamName = team.getName();
assertTrue("Team " + teamName + " in organization " + organizationName assertTrue("Team " + teamName + " in organization " + organizationName
+ " does not contain myself", + " does not contain myself",
shouldBelongToTeam(organizationName, teamName)); shouldBelongToTeam(organizationName, teamName));
}
} }
}
} }
private boolean shouldBelongToTeam(String organizationName, String teamName) throws IOException { private boolean shouldBelongToTeam(String organizationName, String teamName) throws IOException {
GHOrganization org = gitHub.getOrganization(organizationName); GHOrganization org = gitHub.getOrganization(organizationName);
assertNotNull(org); assertNotNull(org);
GHTeam team = org.getTeamByName(teamName); GHTeam team = org.getTeamByName(teamName);
assertNotNull(team); assertNotNull(team);
return team.hasMember(gitHub.getMyself()); return team.hasMember(gitHub.getMyself());
} }
@Test @Test
@@ -595,10 +598,10 @@ public class AppTest extends AbstractGitHubApiTestBase {
} }
@Test @Test
public void testRef() throws IOException { public void testRef() throws IOException {
GHRef masterRef = gitHub.getRepository("jenkinsci/jenkins").getRef("heads/master"); GHRef masterRef = gitHub.getRepository("jenkinsci/jenkins").getRef("heads/master");
assertEquals("https://api.github.com/repos/jenkinsci/jenkins/git/refs/heads/master", masterRef.getUrl().toString()); assertEquals("https://api.github.com/repos/jenkinsci/jenkins/git/refs/heads/master", masterRef.getUrl().toString());
} }
@Test @Test
public void directoryListing() throws IOException { public void directoryListing() throws IOException {
@@ -615,8 +618,8 @@ public class AppTest extends AbstractGitHubApiTestBase {
@Test @Test
public void testAddDeployKey() throws IOException { public void testAddDeployKey() throws IOException {
GHRepository myRepository = Iterables.get(gitHub.getMyself().getRepositories().values(),0); GHRepository myRepository = Iterables.get(gitHub.getMyself().getRepositories().values(),0);
final GHDeployKey newDeployKey = myRepository.addDeployKey("test", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUt0RAycC5cS42JKh6SecfFZBR1RrF+2hYMctz4mk74/arBE+wFb7fnSHGzdGKX2h5CFOWODifRCJVhB7hlVxodxe+QkQQYAEL/x1WVCJnGgTGQGOrhOMj95V3UE5pQKhsKD608C+u5tSofcWXLToP1/wZ7U4/AHjqYi08OLsWToHCax55TZkvdt2jo0hbIoYU+XI9Q8Uv4ONDN1oabiOdgeKi8+crvHAuvNleiBhWVBzFh8KdfzaH5uNdw7ihhFjEd1vzqACsjCINCjdMfzl6jD9ExuWuE92nZJnucls2cEoNC6k2aPmrZDg9hA32FXVpyseY+bDUWFU6LO2LG6PB kohsuke@atlas"); final GHDeployKey newDeployKey = myRepository.addDeployKey("test", "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUt0RAycC5cS42JKh6SecfFZBR1RrF+2hYMctz4mk74/arBE+wFb7fnSHGzdGKX2h5CFOWODifRCJVhB7hlVxodxe+QkQQYAEL/x1WVCJnGgTGQGOrhOMj95V3UE5pQKhsKD608C+u5tSofcWXLToP1/wZ7U4/AHjqYi08OLsWToHCax55TZkvdt2jo0hbIoYU+XI9Q8Uv4ONDN1oabiOdgeKi8+crvHAuvNleiBhWVBzFh8KdfzaH5uNdw7ihhFjEd1vzqACsjCINCjdMfzl6jD9ExuWuE92nZJnucls2cEoNC6k2aPmrZDg9hA32FXVpyseY+bDUWFU6LO2LG6PB kohsuke@atlas");
try { try {
assertNotNull(newDeployKey.getId()); assertNotNull(newDeployKey.getId());
@@ -633,11 +636,11 @@ public class AppTest extends AbstractGitHubApiTestBase {
@Test @Test
public void testCommitStatusContext() throws IOException { public void testCommitStatusContext() throws IOException {
GHRepository myRepository = Iterables.get(gitHub.getMyself().getRepositories().values(), 0); GHRepository myRepository = Iterables.get(gitHub.getMyself().getRepositories().values(), 0);
GHRef masterRef = myRepository.getRef("heads/master"); GHRef masterRef = myRepository.getRef("heads/master");
GHCommitStatus commitStatus = myRepository.createCommitStatus(masterRef.getObject().getSha(), GHCommitState.SUCCESS, "http://www.example.com", "test", "test/context"); GHCommitStatus commitStatus = myRepository.createCommitStatus(masterRef.getObject().getSha(), GHCommitState.SUCCESS, "http://www.example.com", "test", "test/context");
assertEquals("test/context", commitStatus.getContext()); assertEquals("test/context", commitStatus.getContext());
} }
@Test @Test
@@ -658,6 +661,172 @@ public class AppTest extends AbstractGitHubApiTestBase {
} }
} }
@Test // issue #99
public void testReadme() throws IOException {
GHContent readme = gitHub.getRepository("github-api-test-org/test-readme").getReadme();
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 {
GHRepository r = gitHub.getRepository("github-api-test-org/test-labels");
List<GHLabel> lst = r.listLabels().asList();
for (GHLabel l : lst) {
System.out.println(l.getName());
}
assertTrue(lst.size() > 5);
GHLabel e = r.getLabel("enhancement");
assertEquals("enhancement",e.getName());
assertNotNull(e.getUrl());
assertTrue(Pattern.matches("[0-9a-fA-F]{6}",e.getColor()));
{// CRUD
GHLabel t = r.createLabel("test", "123456");
GHLabel t2 = r.getLabel("test");
assertEquals(t.getName(), t2.getName());
assertEquals(t.getColor(), "123456");
assertEquals(t.getColor(), t2.getColor());
assertEquals(t.getUrl(), t2.getUrl());
t.delete();
}
}
@Test
public void testSubscribers() throws IOException {
boolean kohsuke = false;
GHRepository mr = gitHub.getRepository("kohsuke/github-api");
for (GHUser u : mr.listSubscribers()) {
System.out.println(u.getLogin());
kohsuke |= u.getLogin().equals("kohsuke");
}
assertTrue(kohsuke);
System.out.println("---");
boolean githubApi = false;
for (GHRepository r : gitHub.getUser("kohsuke").listRepositories()) {
System.out.println(r.getName());
githubApi |= r.equals(mr);
}
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() { 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

@@ -0,0 +1,16 @@
package org.kohsuke.github;
import org.junit.Test;
import java.io.IOException;
/**
* @author Kohsuke Kawaguchi
*/
public class CommitTest extends AbstractGitHubApiTestBase {
@Test // issue 152
public void lastStatus() throws IOException {
GHTag t = gitHub.getRepository("stapler/stapler").listTags().iterator().next();
t.getCommit().getLastStatus();
}
}

View File

@@ -30,21 +30,21 @@ public class GitHubTest extends TestCase {
} }
public void testGitHubBuilderFromEnvironment() throws IOException { public void testGitHubBuilderFromEnvironment() throws IOException {
Map<String, String>props = new HashMap<String, String>(); Map<String, String>props = new HashMap<String, String>();
props.put("login", "bogus"); props.put("login", "bogus");
props.put("oauth", "bogus"); props.put("oauth", "bogus");
props.put("password", "bogus"); props.put("password", "bogus");
setupEnvironment(props); setupEnvironment(props);
GitHubBuilder builder = GitHubBuilder.fromEnvironment(); GitHubBuilder builder = GitHubBuilder.fromEnvironment();
assertEquals("bogus", builder.user); assertEquals("bogus", builder.user);
assertEquals("bogus", builder.oauthToken); assertEquals("bogus", builder.oauthToken);
assertEquals("bogus", builder.password); assertEquals("bogus", builder.password);
} }
/* /*
@@ -55,58 +55,54 @@ 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); Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null);
Map<String, String> env = (Map<String, String>) theEnvironmentField.get(null); env.putAll(newenv);
env.putAll(newenv); Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment");
Field theCaseInsensitiveEnvironmentField = processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment"); 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) {
} try {
catch (NoSuchFieldException e) Class[] classes = Collections.class.getDeclaredClasses();
{ Map<String, String> env = System.getenv();
try { for (Class cl : classes) {
Class[] classes = Collections.class.getDeclaredClasses(); if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) {
Map<String, String> env = System.getenv(); Field field = cl.getDeclaredField("m");
for(Class cl : classes) { field.setAccessible(true);
if("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { Object obj = field.get(env);
Field field = cl.getDeclaredField("m"); Map<String, String> map = (Map<String, String>) obj;
field.setAccessible(true); map.clear();
Object obj = field.get(env); map.putAll(newenv);
Map<String, String> map = (Map<String, String>) obj; }
map.clear(); }
map.putAll(newenv); } catch (Exception e2) {
} e2.printStackTrace();
} }
} catch (Exception e2) { } catch (Exception e1) {
e2.printStackTrace(); e1.printStackTrace();
} }
} catch (Exception e1) {
e1.printStackTrace();
}
}
public void testGitHubBuilderFromCustomEnvironment() throws IOException {
Map<String, String>props = new HashMap<String, String>();
props.put("customLogin", "bogusLogin");
props.put("customOauth", "bogusOauth");
props.put("customPassword", "bogusPassword");
setupEnvironment(props);
GitHubBuilder builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth");
assertEquals("bogusLogin", builder.user);
assertEquals("bogusOauth", builder.oauthToken);
assertEquals("bogusPassword", builder.password);
} }
public void testGitHubBuilderFromCustomEnvironment() throws IOException {
Map<String, String> props = new HashMap<String, String>();
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", "customEndpoint");
assertEquals("bogusLogin", builder.user);
assertEquals("bogusOauth", builder.oauthToken);
assertEquals("bogusPassword", builder.password);
assertEquals("bogusEndpoint", builder.endpoint);
}
} }

View File

@@ -24,7 +24,7 @@ public class PullRequestTest extends AbstractGitHubApiTestBase {
String label = rnd.next(); String label = rnd.next();
p.setLabels(label); p.setLabels(label);
Collection<GHIssue.Label> labels = getRepository().getPullRequest(p.getNumber()).getLabels(); Collection<GHLabel> labels = getRepository().getPullRequest(p.getNumber()).getLabels();
assertEquals(1, labels.size()); assertEquals(1, labels.size());
assertEquals(label, labels.iterator().next().getName()); assertEquals(label, labels.iterator().next().getName());
} }
@@ -38,6 +38,22 @@ public class PullRequestTest extends AbstractGitHubApiTestBase {
assertEquals(user, getRepository().getPullRequest(p.getNumber()).getAssignee()); assertEquals(user, getRepository().getPullRequest(p.getNumber()).getAssignee());
} }
@Test
public void testGetUser() throws IOException {
GHPullRequest p = getRepository().createPullRequest(rnd.next(), "stable", "master", "## test");
GHPullRequest prSingle = getRepository().getPullRequest(p.getNumber());
assertNotNull(prSingle.getUser().root);
prSingle.getMergeable();
assertNotNull(prSingle.getUser().root);
PagedIterable<GHPullRequest> ghPullRequests = getRepository().listPullRequests(GHIssueState.OPEN);
for (GHPullRequest pr : ghPullRequests) {
assertNotNull(pr.getUser().root);
assertFalse(pr.getMergeable());
assertNotNull(pr.getUser().root);
}
}
@After @After
public void cleanUp() throws Exception { public void cleanUp() throws Exception {
for (GHPullRequest pr : getRepository().getPullRequests(GHIssueState.OPEN)) { for (GHPullRequest pr : getRepository().getPullRequests(GHIssueState.OPEN)) {

View File

@@ -0,0 +1,89 @@
package org.kohsuke.github;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.Iterator;
import static org.mockito.Mockito.when;
/**
* @author Luciano P. Sabenca (luciano.sabenca [at] movile [com] | lucianosabenca [at] gmail [dot] com
*/
public class RepositoryMockTest {
@Mock
GitHub mockGitHub;
@Mock
Iterator<GHUser[]> iterator;
@Mock
GHRepository mockRepository;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void listCollaborators() throws Exception {
GHUser user1 = new GHUser();
user1.login = "login1";
GHUser user2 = new GHUser();
user2.login = "login2";
when(iterator.hasNext()).thenReturn(true, false, true);
when(iterator.next()).thenReturn(new GHUser[]{user1}, new GHUser[]{user2});
Requester requester = Mockito.mock(Requester.class);
when(mockGitHub.retrieve()).thenReturn(requester);
when(requester.asIterator("/repos/*/*/collaborators",
GHUser[].class)).thenReturn(iterator, iterator);
PagedIterable<GHUser> pagedIterable = Mockito.mock(PagedIterable.class);
when(mockRepository.listCollaborators()).thenReturn(pagedIterable);
PagedIterator<GHUser> userPagedIterator = new PagedIterator<GHUser>(iterator) {
@Override
protected void wrapUp(GHUser[] page) {
}
};
PagedIterator<GHUser> userPagedIterator2 = new PagedIterator<GHUser>(iterator) {
@Override
protected void wrapUp(GHUser[] page) {
}
};
when(pagedIterable.iterator()).thenReturn(userPagedIterator, userPagedIterator2);
Iterator<GHUser> returnIterator1 = mockRepository.listCollaborators().iterator();
Assert.assertTrue(returnIterator1.hasNext());
GHUser user = returnIterator1.next();
Assert.assertEquals(user, user1);
Assert.assertFalse(returnIterator1.hasNext());
Iterator returnIterator2 = mockRepository.listCollaborators().iterator();
Assert.assertTrue(returnIterator2.hasNext());
user = returnIterator1.next();
Assert.assertEquals(user, user2);
}
}

View File

@@ -1,91 +1,53 @@
package org.kohsuke.github; package org.kohsuke.github;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mock; import org.kohsuke.github.GHRepository.Contributor;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.Iterator; import java.io.IOException;
import static org.mockito.Mockito.when;
/** /**
* @author Luciano P. Sabenca (luciano.sabenca [at] movile [com] | lucianosabenca [at] gmail [dot] com * @author Kohsuke Kawaguchi
*/ */
public class RepositoryTest { public class RepositoryTest extends AbstractGitHubApiTestBase {
@Test
public void subscription() throws Exception {
GHRepository r = getRepository();
assertNull(r.getSubscription());
@Mock GHSubscription s = r.subscribe(true, false);
GitHub mockGitHub; assertEquals(s.getRepository(), r);
@Mock s.delete();
Iterator<GHUser[]> iterator;
@Mock assertNull(r.getSubscription());
GHRepository mockRepository;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
} }
@Test @Test
public void listCollaborators() throws Exception { public void listContributors() throws IOException {
GHUser user1 = new GHUser(); GHRepository r = gitHub.getOrganization("stapler").getRepository("stapler");
user1.login = "login1"; int i=0;
boolean kohsuke = false;
GHUser user2 = new GHUser(); for (Contributor c : r.listContributors()) {
user2.login = "login2"; System.out.println(c.getName());
assertTrue(c.getContributions()>0);
if (c.getLogin().equals("kohsuke"))
kohsuke = true;
if (i++ > 5)
break;
}
assertTrue(kohsuke);
}
when(iterator.hasNext()).thenReturn(true, false, true); private GHRepository getRepository() throws IOException {
when(iterator.next()).thenReturn(new GHUser[]{user1}, new GHUser[]{user2}); return gitHub.getOrganization("github-api-test-org").getRepository("jenkins");
}
Requester requester = Mockito.mock(Requester.class);
when(mockGitHub.retrieve()).thenReturn(requester);
when(requester.asIterator("/repos/*/*/collaborators",
GHUser[].class)).thenReturn(iterator, iterator);
PagedIterable<GHUser> pagedIterable = Mockito.mock(PagedIterable.class);
when(mockRepository.listCollaborators()).thenReturn(pagedIterable);
PagedIterator<GHUser> userPagedIterator = new PagedIterator<GHUser>(iterator) {
@Override
protected void wrapUp(GHUser[] page) {
}
};
PagedIterator<GHUser> userPagedIterator2 = new PagedIterator<GHUser>(iterator) {
@Override
protected void wrapUp(GHUser[] page) {
}
};
when(pagedIterable.iterator()).thenReturn(userPagedIterator, userPagedIterator2);
Iterator<GHUser> returnIterator1 = mockRepository.listCollaborators().iterator();
Assert.assertTrue(returnIterator1.hasNext());
GHUser user = returnIterator1.next();
Assert.assertEquals(user, user1);
Assert.assertFalse(returnIterator1.hasNext());
Iterator returnIterator2 = mockRepository.listCollaborators().iterator();
Assert.assertTrue(returnIterator2.hasNext());
user = returnIterator1.next();
Assert.assertEquals(user, user2);
@Test
public void listLanguages() throws IOException {
GHRepository r = gitHub.getRepository("kohsuke/github-api");
String mainLanguage = r.getLanguage();
assertTrue(r.listLanguages().containsKey(mainLanguage));
} }
} }