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",