diff --git a/pom.xml b/pom.xml index a2254c512d..bd00bca40c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.81 + 1.82 GitHub API for Java http://github-api.kohsuke.org/ GitHub API for Java @@ -16,7 +16,7 @@ scm:git:git@github.com/kohsuke/${project.artifactId}.git scm:git:ssh://git@github.com/kohsuke/${project.artifactId}.git http://${project.artifactId}.kohsuke.org/ - github-api-1.81 + github-api-1.82 diff --git a/src/main/java/org/kohsuke/github/GHBlob.java b/src/main/java/org/kohsuke/github/GHBlob.java new file mode 100644 index 0000000000..a38a18219c --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHBlob.java @@ -0,0 +1,64 @@ +package org.kohsuke.github; + +import org.apache.commons.codec.binary.Base64InputStream; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URL; + +/** + * @author Kanstantsin Shautsou + * @author Kohsuke Kawaguchi + * @see GHTreeEntry#asBlob() + * @see GHRepository#getBlob(String) + * @see Get a blob + */ +public class GHBlob { + private String content, encoding, url, sha; + private long size; + + /** + * API URL of this blob. + */ + public URL getUrl() { + return GitHub.parseURL(url); + } + + public String getSha() { + return sha; + } + + /** + * Number of bytes in this blob. + */ + public long getSize() { + return size; + } + + public String getEncoding() { + return encoding; + } + + /** + * Encoded content. You probably want {@link #read()} + */ + public String getContent() { + return content; + } + + /** + * Retrieves the actual bytes of the blob. + */ + public InputStream read() { + if (encoding.equals("base64")) { + try { + return new Base64InputStream(new ByteArrayInputStream(content.getBytes("US-ASCII")), false); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); // US-ASCII is mandatory + } + } + + throw new UnsupportedOperationException("Unrecognized encoding: "+encoding); + } +} diff --git a/src/main/java/org/kohsuke/github/GHCommit.java b/src/main/java/org/kohsuke/github/GHCommit.java index 3740778480..ec5dfbffc1 100644 --- a/src/main/java/org/kohsuke/github/GHCommit.java +++ b/src/main/java/org/kohsuke/github/GHCommit.java @@ -37,6 +37,12 @@ public static class ShortInfo { private int comment_count; + static class Tree { + String sha; + } + + private Tree tree; + @WithBridgeMethods(value = GHAuthor.class, castRequired = true) public GitUser getAuthor() { return author; @@ -224,6 +230,13 @@ public int getLinesDeleted() throws IOException { return stats.deletions; } + /** + * Use this method to walk the tree + */ + public GHTree getTree() throws IOException { + return owner.getTree(getCommitShortInfo().tree.sha); + } + /** * URL of this commit like "https://github.com/kohsuke/sandbox-ant/commit/8ae38db0ea5837313ab5f39d43a6f73de3bd9000" */ diff --git a/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java b/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java index ff574025c0..f9634a59f1 100644 --- a/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java @@ -41,6 +41,11 @@ public GHIssueSearchBuilder isMerged() { return q("is:merged"); } + public GHIssueSearchBuilder order(GHDirection v) { + req.with("order",v); + return this; + } + public GHIssueSearchBuilder sort(Sort sort) { req.with("sort",sort); return this; diff --git a/src/main/java/org/kohsuke/github/GHPerson.java b/src/main/java/org/kohsuke/github/GHPerson.java index 67e157173f..f9872699ce 100644 --- a/src/main/java/org/kohsuke/github/GHPerson.java +++ b/src/main/java/org/kohsuke/github/GHPerson.java @@ -93,10 +93,10 @@ protected void wrapUp(GHRepository[] page) { } /** - * Loads repository list in a pagenated fashion. + * Loads repository list in a paginated fashion. * *

- * For a person with a lot of repositories, GitHub returns the list of repositories in a pagenated fashion. + * For a person with a lot of repositories, GitHub returns the list of repositories in a paginated fashion. * Unlike {@link #getRepositories()}, this method allows the caller to start processing data as it arrives. * * Every {@link Iterator#next()} call results in I/O. Exceptions that occur during the processing is wrapped diff --git a/src/main/java/org/kohsuke/github/GHPullRequest.java b/src/main/java/org/kohsuke/github/GHPullRequest.java index a6becf495c..04898039bc 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequest.java +++ b/src/main/java/org/kohsuke/github/GHPullRequest.java @@ -213,7 +213,7 @@ private void populate() throws IOException { public PagedIterable listFiles() { return new PagedIterable() { public PagedIterator _iterator(int pageSize) { - return new PagedIterator(root.retrieve().asIterator(String.format("%s/files", getApiURL()), + return new PagedIterator(root.retrieve().asIterator(String.format("%s/files", getApiRoute()), GHPullRequestFileDetail[].class, pageSize)) { @Override protected void wrapUp(GHPullRequestFileDetail[] page) { @@ -247,7 +247,7 @@ public PagedIterable listCommits() { return new PagedIterable() { public PagedIterator _iterator(int pageSize) { return new PagedIterator(root.retrieve().asIterator( - String.format("%s/commits", getApiURL()), + String.format("%s/commits", getApiRoute()), GHPullRequestCommitDetail[].class, pageSize)) { @Override protected void wrapUp(GHPullRequestCommitDetail[] page) { diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index f34ea85004..4a0d410621 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -31,6 +31,7 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.InterruptedIOException; import java.io.Reader; @@ -504,7 +505,6 @@ public void removeCollaborators(Collection users) throws IOException { } private void modifyCollaborators(Collection users, String method) throws IOException { - verifyMine(); for (GHUser user : users) { new Requester(root).method(method).to(getApiTailUrl("collaborators/" + user.getLogin())); } @@ -799,7 +799,7 @@ public GHRef getRef(String refName) throws IOException { */ public GHTree getTree(String sha) throws IOException { String url = String.format("/repos/%s/%s/git/trees/%s", getOwnerName(), name, sha); - return root.retrieve().to(url, GHTree.class).wrap(root); + return root.retrieve().to(url, GHTree.class).wrap(this); } /** @@ -814,7 +814,32 @@ public GHTree getTree(String sha) throws IOException { */ public GHTree getTreeRecursive(String sha, int recursive) throws IOException { String url = String.format("/repos/%s/%s/git/trees/%s?recursive=%d", getOwnerName(), name, sha, recursive); - return root.retrieve().to(url, GHTree.class).wrap(root); + return root.retrieve().to(url, GHTree.class).wrap(this); + } + + /** + * Obtains the metadata & the content of a blob. + * + *

+ * This method retrieves the whole content in memory, so beware when you are dealing with large BLOB. + * + * @see Get a blob + * @see #readBlob(String) + */ + public GHBlob getBlob(String blobSha) throws IOException { + String target = getApiTailUrl("git/blobs/" + blobSha); + return root.retrieve().to(target, GHBlob.class); + } + + /** + * Reads the content of a blob as a stream for better efficiency. + * + * @see Get a blob + * @see #getBlob(String) + */ + public InputStream readBlob(String blobSha) throws IOException { + String target = getApiTailUrl("git/blobs/" + blobSha); + return root.retrieve().withHeader("Accept","application/vnd.github.VERSION.raw").asStream(target); } /** @@ -1092,11 +1117,6 @@ public GHHook createWebHook(URL url) throws IOException { // return root.retrieveWithAuth("/pulls/"+owner+'/'+name,JsonPullRequests.class).wrap(root); // } - private void verifyMine() throws IOException { - if (!root.login.equals(getOwnerName())) - throw new IOException("Operation not applicable to a repository owned by someone else: " + getOwnerName()); - } - /** * Returns a set that represents the post-commit hook URLs. * The returned set is live, and changes made to them are reflected to GitHub. diff --git a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java index 8d30aaaf6b..03b12f5133 100644 --- a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java @@ -57,6 +57,11 @@ public GHRepositorySearchBuilder stars(String v) { return q("stars:"+v); } + public GHRepositorySearchBuilder order(GHDirection v) { + req.with("order",v); + return this; + } + public GHRepositorySearchBuilder sort(Sort sort) { req.with("sort",sort); return this; diff --git a/src/main/java/org/kohsuke/github/GHTree.java b/src/main/java/org/kohsuke/github/GHTree.java index aaf98e77e2..59c7cd287e 100644 --- a/src/main/java/org/kohsuke/github/GHTree.java +++ b/src/main/java/org/kohsuke/github/GHTree.java @@ -10,10 +10,12 @@ * https://developer.github.com/v3/git/trees/ * * @author Daniel Teixeira - https://github.com/ddtxra + * @see GHCommit#getTree() * @see GHRepository#getTree(String) + * @see GHTreeEntry#asTree() */ public class GHTree { - /* package almost final */GitHub root; + /* package almost final */GHRepository repo; private boolean truncated; private String sha, url; @@ -34,6 +36,19 @@ public List getTree() { return Collections.unmodifiableList(Arrays.asList(tree)); } + /** + * Finds a tree entry by its name. + * + * IOW, find a directory entry by a file name. + */ + public GHTreeEntry getEntry(String path) { + for (GHTreeEntry e : tree) { + if (e.getPath().equals(path)) + return e; + } + return null; + } + /** * 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. @@ -50,8 +65,11 @@ public URL getUrl() { return GitHub.parseURL(url); } - /* package */GHTree wrap(GitHub root) { - this.root = root; + /* package */GHTree wrap(GHRepository repo) { + this.repo = repo; + for (GHTreeEntry e : tree) { + e.tree = this; + } return this; } diff --git a/src/main/java/org/kohsuke/github/GHTreeEntry.java b/src/main/java/org/kohsuke/github/GHTreeEntry.java index e3d831c073..ba6d45e15e 100644 --- a/src/main/java/org/kohsuke/github/GHTreeEntry.java +++ b/src/main/java/org/kohsuke/github/GHTreeEntry.java @@ -1,5 +1,7 @@ package org.kohsuke.github; +import java.io.IOException; +import java.io.InputStream; import java.net.URL; /** @@ -10,6 +12,8 @@ * @see GHTree */ public class GHTreeEntry { + /* package almost final */GHTree tree; + private String path, mode, type, sha, url; private long size; @@ -44,7 +48,7 @@ public long getSize() { /** * Gets the type such as: - * "blob" + * "blob", "tree", etc. * * @return The type */ @@ -68,4 +72,37 @@ public String getSha() { public URL getUrl() { return GitHub.parseURL(url); } + + /** + * If this tree entry represents a file, then return its information. + * Otherwise null. + */ + public GHBlob asBlob() throws IOException { + if (type.equals("blob")) + return tree.repo.getBlob(sha); + else + return null; + } + + /** + * If this tree entry represents a file, then return its content. + * Otherwise null. + */ + public InputStream readAsBlob() throws IOException { + if (type.equals("blob")) + return tree.repo.readBlob(sha); + else + return null; + } + + /** + * If this tree entry represents a directory, then return it. + * Otherwise null. + */ + public GHTree asTree() throws IOException { + if (type.equals("tree")) + return tree.repo.getTree(sha); + else + return null; + } } diff --git a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java index fa5161683f..ca40f4d050 100644 --- a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java @@ -49,6 +49,11 @@ public GHUserSearchBuilder followers(String v) { return q("followers:"+v); } + public GHUserSearchBuilder order(GHDirection v) { + req.with("order",v); + return this; + } + public GHUserSearchBuilder sort(Sort sort) { req.with("sort",sort); return this; diff --git a/src/main/java/org/kohsuke/github/PagedIterator.java b/src/main/java/org/kohsuke/github/PagedIterator.java index 9149290911..aaa8182076 100644 --- a/src/main/java/org/kohsuke/github/PagedIterator.java +++ b/src/main/java/org/kohsuke/github/PagedIterator.java @@ -6,7 +6,7 @@ import java.util.NoSuchElementException; /** - * Iterator over a pagenated data source. + * Iterator over a paginated data source. * * Aside from the normal iterator operation, this method exposes {@link #nextPage()} * that allows the caller to retrieve items per page. diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 3d240abd65..70c48940c0 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -355,7 +355,7 @@ private boolean isMethodWithBody() { } /** - * Loads pagenated resources. + * Loads paginated resources. * * Every iterator call reports a new batch. */ diff --git a/src/test/java/org/kohsuke/github/AppTest.java b/src/test/java/org/kohsuke/github/AppTest.java index 0a958418be..e1d513d739 100755 --- a/src/test/java/org/kohsuke/github/AppTest.java +++ b/src/test/java/org/kohsuke/github/AppTest.java @@ -12,6 +12,7 @@ import org.kohsuke.github.GHOrganization.Permission; import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.util.*; import java.util.Map.Entry; @@ -362,6 +363,11 @@ public void testCommit() throws Exception { assertEquals(48,f.getLinesChanged()); assertEquals("modified",f.getStatus()); assertEquals("changelog.html", f.getFileName()); + + // walk the tree + GHTree t = commit.getTree(); + assertThat(IOUtils.toString(t.getEntry("todo.txt").readAsBlob()), containsString("executor rendering")); + assertNotNull(t.getEntry("war").asTree()); } @Test @@ -903,6 +909,26 @@ public void listOrgMemberships() throws Exception { } } + @Test + public void blob() throws Exception { + GHRepository r = gitHub.getRepository("kohsuke/github-api"); + String sha1 = "a12243f2fc5b8c2ba47dd677d0b0c7583539584d"; + + assertBlobContent(r.readBlob(sha1)); + + GHBlob blob = r.getBlob(sha1); + assertBlobContent(blob.read()); + assertThat(blob.getSha(),is("a12243f2fc5b8c2ba47dd677d0b0c7583539584d")); + assertThat(blob.getSize(),is(1104L)); + } + + private void assertBlobContent(InputStream is) throws Exception { + String content = new String(IOUtils.toByteArray(is),"UTF-8"); + assertThat(content,containsString("Copyright (c) 2011- Kohsuke Kawaguchi and other contributors")); + assertThat(content,containsString("FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR")); + assertThat(content.length(),is(1104)); + } + private void kohsuke() { String login = getUser().getLogin(); Assume.assumeTrue(login.equals("kohsuke") || login.equals("kohsuke2")); diff --git a/src/test/resources/org/kohsuke/github/GHEventPayloadTest/label.json b/src/test/resources/org/kohsuke/github/GHEventPayloadTest/label.json index f85f636f89..b7a0be1b2e 100644 --- a/src/test/resources/org/kohsuke/github/GHEventPayloadTest/label.json +++ b/src/test/resources/org/kohsuke/github/GHEventPayloadTest/label.json @@ -65,8 +65,7 @@ "issues_url": "https://api.github.com/repos/baxterandthehackers/public-repo/issues{/number}", "pulls_url": "https://api.github.com/repos/baxterandthehackers/public-repo/pulls{/number}", "milestones_url": "https://api.github.com/repos/baxterandthehackers/public-repo/milestones{/number}", - "notifications_url": "https://api.github.com/repos/baxterandthehackers/public-repo/notifications{?since,all, - participating}", + "notifications_url": "https://api.github.com/repos/baxterandthehackers/public-repo/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/baxterandthehackers/public-repo/labels{/name}", "releases_url": "https://api.github.com/repos/baxterandthehackers/public-repo/releases{/id}", "deployments_url": "https://api.github.com/repos/baxterandthehackers/public-repo/deployments", diff --git a/src/test/resources/org/kohsuke/github/GHEventPayloadTest/milestone.json b/src/test/resources/org/kohsuke/github/GHEventPayloadTest/milestone.json index 7f262abd5c..1cd673eef5 100644 --- a/src/test/resources/org/kohsuke/github/GHEventPayloadTest/milestone.json +++ b/src/test/resources/org/kohsuke/github/GHEventPayloadTest/milestone.json @@ -2,8 +2,7 @@ "action": "created", "milestone": { "url": "https://api.github.com/repos/baxterandthehackers/public-repo/milestones/3", - "html_url": "https://github.com/baxterandthehackers/public-repo/milestones/Test%20milestone%20creation%20webhook - %20from%20command%20line2", + "html_url": "https://github.com/baxterandthehackers/public-repo/milestones/Test%20milestone%20creation%20webhook%20from%20command%20line2", "labels_url": "https://api.github.com/repos/baxterandthehackers/public-repo/milestones/3/labels", "id": 2055681, "number": 3, @@ -96,8 +95,7 @@ "issues_url": "https://api.github.com/repos/baxterandthehackers/public-repo/issues{/number}", "pulls_url": "https://api.github.com/repos/baxterandthehackers/public-repo/pulls{/number}", "milestones_url": "https://api.github.com/repos/baxterandthehackers/public-repo/milestones{/number}", - "notifications_url": "https://api.github.com/repos/baxterandthehackers/public-repo/notifications{?since,all, - participating}", + "notifications_url": "https://api.github.com/repos/baxterandthehackers/public-repo/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/baxterandthehackers/public-repo/labels{/name}", "releases_url": "https://api.github.com/repos/baxterandthehackers/public-repo/releases{/id}", "deployments_url": "https://api.github.com/repos/baxterandthehackers/public-repo/deployments",