diff --git a/pom.xml b/pom.xml
index 6f2201f303..e0b19ded3d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
github-api
- 1.22
+ 1.23
GitHub API for Java
http://github-api.kohsuke.org/
GitHub API for Java
diff --git a/src/main/java/org/kohsuke/github/GHCommit.java b/src/main/java/org/kohsuke/github/GHCommit.java
new file mode 100644
index 0000000000..9ba4ca2bd8
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHCommit.java
@@ -0,0 +1,243 @@
+package org.kohsuke.github;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.kohsuke.github.ApiVersion.V3;
+
+/**
+ * A commit in a repository.
+ *
+ * @author Kohsuke Kawaguchi
+ * @see GHRepository#getCommit(String)
+ * @see GHCommitComment#getCommit()
+ */
+public class GHCommit {
+ private GHRepository owner;
+
+ public static class Stats {
+ int total,additions,deletions;
+ }
+
+ /**
+ * A file that was modified.
+ */
+ public static class File {
+ String status;
+ int changes,additions,deletions;
+ String raw_url, blob_url, filename, sha, patch;
+
+ /**
+ * Number of lines added + removed.
+ */
+ public int getLinesChanged() {
+ return changes;
+ }
+
+ /**
+ * Number of lines added.
+ */
+ public int getLinesAdded() {
+ return additions;
+ }
+
+ /**
+ * Number of lines removed.
+ */
+ public int getLinesDeleted() {
+ return deletions;
+ }
+
+ /**
+ * "modified", "added", or "deleted"
+ */
+ public String getStatus() {
+ return status;
+ }
+
+ /**
+ * Just the base name and the extension without any directory name.
+ */
+ public String getFileName() {
+ return filename;
+ }
+
+ /**
+ * The actual change.
+ */
+ public String getPatch() {
+ return patch;
+ }
+
+ /**
+ * URL like 'https://raw.github.com/jenkinsci/jenkins/4eb17c197dfdcf8ef7ff87eb160f24f6a20b7f0e/core/pom.xml'
+ * that resolves to the actual content of the file.
+ */
+ public URL getRawUrl() {
+ return GitHub.parseURL(raw_url);
+ }
+
+ /**
+ * URL like 'https://github.com/jenkinsci/jenkins/blob/1182e2ebb1734d0653142bd422ad33c21437f7cf/core/pom.xml'
+ * that resolves to the HTML page that describes this file.
+ */
+ public URL getBlobUrl() {
+ return GitHub.parseURL(blob_url);
+ }
+
+ /**
+ * [0-9a-f]{40} SHA1 checksum.
+ */
+ public String getSha() {
+ return sha;
+ }
+ }
+
+ public static class Parent {
+ String url,sha;
+ }
+
+ static class User {
+ // TODO: what if someone who doesn't have an account on GitHub makes a commit?
+ String url,avatar_url,login,gravatar_id;
+ int id;
+ }
+
+ String url,sha;
+ List files;
+ Stats stats;
+ List parents;
+ User author,committer;
+
+ /**
+ * The repository that contains the commit.
+ */
+ public GHRepository getOwner() {
+ return owner;
+ }
+
+ /**
+ * Number of lines added + removed.
+ */
+ public int getLinesChanged() {
+ return stats.total;
+ }
+
+ /**
+ * Number of lines added.
+ */
+ public int getLinesAdded() {
+ return stats.additions;
+ }
+
+ /**
+ * Number of lines removed.
+ */
+ public int getLinesDeleted() {
+ return stats.deletions;
+ }
+
+ /**
+ * [0-9a-f]{40} SHA1 checksum.
+ */
+ public String getSHA1() {
+ return sha;
+ }
+
+ /**
+ * List of files changed/added/removed in this commit.
+ *
+ * @return
+ * Can be empty but never null.
+ */
+ public List getFiles() {
+ return files!=null ? Collections.unmodifiableList(files) : Collections.emptyList();
+ }
+
+ /**
+ * Returns the SHA1 of parent commit objects.
+ */
+ public List getParentSHA1s() {
+ if (parents==null) return Collections.emptyList();
+ return new AbstractList() {
+ @Override
+ public String get(int index) {
+ return parents.get(index).sha;
+ }
+
+ @Override
+ public int size() {
+ return parents.size();
+ }
+ };
+ }
+
+ /**
+ * Resolves the parent commit objects and return them.
+ */
+ public List getParents() throws IOException {
+ List r = new ArrayList();
+ for (String sha1 : getParentSHA1s())
+ r.add(owner.getCommit(sha1));
+ return r;
+ }
+
+ public GHUser getAuthor() throws IOException {
+ return resolveUser(author);
+ }
+
+ public GHUser getCommitter() throws IOException {
+ return resolveUser(committer);
+ }
+
+ private GHUser resolveUser(User author) throws IOException {
+ if (author==null || author.login==null) return null;
+ return owner.root.getUser(author.login);
+ }
+
+ /**
+ * Lists up all the commit comments in this repository.
+ */
+ public PagedIterable listComments() {
+ return new PagedIterable() {
+ public PagedIterator iterator() {
+ return new PagedIterator(owner.root.retrievePaged(String.format("/repos/%s/%s/commits/%s/comments",owner.getOwnerName(),owner.getName(),sha),GHCommitComment[].class,false,V3)) {
+ @Override
+ protected void wrapUp(GHCommitComment[] page) {
+ for (GHCommitComment c : page)
+ c.wrap(owner);
+ }
+ };
+ }
+ };
+ }
+
+ /**
+ * Creates a commit comment.
+ *
+ * I'm not sure how path/line/position parameters interact with each other.
+ */
+ public GHCommitComment createComment(String body, String path, Integer line, Integer position) throws IOException {
+ GHCommitComment r = new Poster(owner.root,V3)
+ .with("body",body)
+ .with("path",path)
+ .with("line",line)
+ .with("position",position)
+ .withCredential()
+ .to(String.format("/repos/%s/%s/commits/%s/comments",owner.getOwnerName(),owner.getName(),sha),GHCommitComment.class);
+ return r.wrap(owner);
+ }
+
+ public GHCommitComment createComment(String body) throws IOException {
+ return createComment(body,null,null,null);
+ }
+
+ GHCommit wrapUp(GHRepository owner) {
+ this.owner = owner;
+ return this;
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GHCommitComment.java b/src/main/java/org/kohsuke/github/GHCommitComment.java
new file mode 100644
index 0000000000..c2364a23d5
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GHCommitComment.java
@@ -0,0 +1,125 @@
+package org.kohsuke.github;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Date;
+
+import static org.kohsuke.github.ApiVersion.V3;
+
+/**
+ * A comment attached to a commit (or a specific line in a specific file of a commit.)
+ *
+ * @author Kohsuke Kawaguchi
+ * @see GHRepository#listCommitComments()
+ * @see GHCommit#listComments()
+ * @see GHCommit#createComment(String, String, Integer, Integer)
+ */
+public class GHCommitComment {
+ private GHRepository owner;
+
+ String updated_at, created_at;
+ String body, url, html_url, commit_id;
+ Integer line;
+ int id;
+ String path;
+ User user;
+
+ static class User {
+ // TODO: what if someone who doesn't have an account on GitHub makes a commit?
+ String url,avatar_url,login,gravatar_id;
+ int id;
+ }
+
+ public GHRepository getOwner() {
+ return owner;
+ }
+
+ public Date getCreatedAt() {
+ return GitHub.parseDate(created_at);
+ }
+
+ public Date getUpdatedAt() {
+ return GitHub.parseDate(updated_at);
+ }
+
+ /**
+ * URL like 'https://github.com/kohsuke/sandbox-ant/commit/8ae38db0ea5837313ab5f39d43a6f73de3bd9000#commitcomment-1252827' to
+ * show this commit comment in a browser.
+ */
+ public URL getHtmlUrl() {
+ return GitHub.parseURL(html_url);
+ }
+
+ public String getSHA1() {
+ return commit_id;
+ }
+
+ /**
+ * Commit comment in the GitHub flavored markdown format.
+ */
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * A commit comment can be on a specific line of a specific file, if so, this field points to a file.
+ * Otherwise null.
+ */
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * A commit comment can be on a specific line of a specific file, if so, this field points to the line number in the file.
+ * Otherwise -1.
+ */
+ public int getLine() {
+ return line!=null ? line : -1;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ /**
+ * Gets the user who put this comment.
+ */
+ public GHUser getUser() throws IOException {
+ return owner.root.getUser(user.login);
+ }
+
+ /**
+ * Gets the commit to which this comment is associated with.
+ */
+ public GHCommit getCommit() throws IOException {
+ return getOwner().getCommit(getSHA1());
+ }
+
+ /**
+ * Updates the body of the commit message.
+ */
+ public void update(String body) throws IOException {
+ GHCommitComment r = new Poster(owner.root,V3)
+ .with("body",body)
+ .withCredential()
+ .to(getApiTail(),GHCommitComment.class,"PATCH");
+ this.body = body;
+ }
+
+ /**
+ * Deletes this comment.
+ */
+ public void delete() throws IOException {
+ new Poster(owner.root,V3).withCredential().to(getApiTail(),null,"DELETE");
+ }
+
+ private String getApiTail() {
+ return String.format("/repos/%s/%s/comments/%s",owner.getOwnerName(),owner.getName(),id);
+ }
+
+
+ GHCommitComment wrap(GHRepository owner) {
+ this.owner = owner;
+ return this;
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GHPerson.java b/src/main/java/org/kohsuke/github/GHPerson.java
index 5a03ed9451..70ba8f27d0 100644
--- a/src/main/java/org/kohsuke/github/GHPerson.java
+++ b/src/main/java/org/kohsuke/github/GHPerson.java
@@ -1,5 +1,7 @@
package org.kohsuke.github;
+import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
+
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java
index 2a2c4f8327..909554a6b0 100644
--- a/src/main/java/org/kohsuke/github/GHRepository.java
+++ b/src/main/java/org/kohsuke/github/GHRepository.java
@@ -73,6 +73,7 @@ public class GHRepository {
private Map milestones = new HashMap();
private String master_branch;
+ private Map commits = new HashMap();
public String getDescription() {
return description;
@@ -271,7 +272,7 @@ public void delete() throws IOException {
String url = "/repos/delete/" + owner.login +"/"+name;
DeleteToken token = poster.to(url, DeleteToken.class);
- poster.with("delete_token",token.delete_token).to(url);
+ poster.with("delete_token", token.delete_token).to(url);
}
/**
@@ -360,6 +361,51 @@ public GHHook getHook(int id) throws IOException {
return root.retrieveWithAuth3(String.format("/repos/%s/%s/hooks/%d",owner.login,name,id),GHHook.class).wrap(this);
}
+ /**
+ * Gets a commit object in this repository.
+ */
+ public GHCommit getCommit(String sha1) throws IOException {
+ GHCommit c = commits.get(sha1);
+ if (c==null) {
+ c = root.retrieve3(String.format("/repos/%s/%s/commits/%s",owner.login,name,sha1),GHCommit.class).wrapUp(this);
+ commits.put(sha1,c);
+ }
+ return c;
+ }
+
+ /**
+ * Lists all the commits.
+ */
+ public PagedIterable listCommits() {
+ return new PagedIterable() {
+ public PagedIterator iterator() {
+ return new PagedIterator(root.retrievePaged(String.format("/repos/%s/%s/commits",owner.login,name),GHCommit[].class,false,V3)) {
+ protected void wrapUp(GHCommit[] page) {
+ for (GHCommit c : page)
+ c.wrapUp(GHRepository.this);
+ }
+ };
+ }
+ };
+ }
+
+ /**
+ * Lists up all the commit comments in this repository.
+ */
+ public PagedIterable listCommitComments() {
+ return new PagedIterable() {
+ public PagedIterator iterator() {
+ return new PagedIterator(root.retrievePaged(String.format("/repos/%s/%s/comments",owner.login,name),GHCommitComment[].class,false,V3)) {
+ @Override
+ protected void wrapUp(GHCommitComment[] page) {
+ for (GHCommitComment c : page)
+ c.wrap(GHRepository.this);
+ }
+ };
+ }
+ };
+ }
+
/**
*
* See https://api.github.com/hooks for possible names and their configuration scheme.
diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java
index 91b34b1d7a..faffd27d79 100644
--- a/src/main/java/org/kohsuke/github/GitHub.java
+++ b/src/main/java/org/kohsuke/github/GitHub.java
@@ -320,7 +320,7 @@ private T parse(HttpURLConnection uc, Class type) throws IOException {
*/
private InputStream wrapStream(HttpURLConnection uc, InputStream in) throws IOException {
String encoding = uc.getContentEncoding();
- if (encoding==null) return in;
+ if (encoding==null || in==null) return in;
if (encoding.equals("gzip")) return new GZIPInputStream(in);
throw new UnsupportedOperationException("Unexpected Content-Encoding: "+encoding);
diff --git a/src/main/java/org/kohsuke/github/PagedIterable.java b/src/main/java/org/kohsuke/github/PagedIterable.java
new file mode 100644
index 0000000000..2470ca7297
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/PagedIterable.java
@@ -0,0 +1,8 @@
+package org.kohsuke.github;
+
+/**
+ * @author Kohsuke Kawaguchi
+ */
+public interface PagedIterable extends Iterable {
+ PagedIterator iterator();
+}
diff --git a/src/main/java/org/kohsuke/github/PagedIterator.java b/src/main/java/org/kohsuke/github/PagedIterator.java
new file mode 100644
index 0000000000..74c197aa2e
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/PagedIterator.java
@@ -0,0 +1,64 @@
+package org.kohsuke.github;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Iterator over a pagenated data source.
+ *
+ * Aside from the normal iterator operation, this method exposes {@link #nextPage()}
+ * that allows the caller to retrieve items per page.
+ *
+ * @author Kohsuke Kawaguchi
+ */
+public abstract class PagedIterator implements Iterator {
+ private final Iterator base;
+
+ /**
+ * Current batch that we retrieved but haven't returned to the caller.
+ */
+ private T[] current;
+ private int pos;
+
+ /*package*/ PagedIterator(Iterator base) {
+ this.base = base;
+ }
+
+ protected abstract void wrapUp(T[] page);
+
+ public boolean hasNext() {
+ return (current!=null && pos nextPage() {
+ fetch();
+ List r = Arrays.asList(current);
+ r = r.subList(pos,r.size());
+ current = null;
+ pos = 0;
+ return r;
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/Poster.java b/src/main/java/org/kohsuke/github/Poster.java
index 556044a986..c07b5ddad3 100644
--- a/src/main/java/org/kohsuke/github/Poster.java
+++ b/src/main/java/org/kohsuke/github/Poster.java
@@ -33,7 +33,9 @@
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Field;
import java.net.HttpURLConnection;
+import java.net.ProtocolException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
@@ -86,6 +88,12 @@ public Poster with(String key, int value) {
return _with(key, value);
}
+ public Poster with(String key, Integer value) {
+ if (value!=null)
+ _with(key, value.intValue());
+ return this;
+ }
+
public Poster with(String key, boolean value) {
return _with(key, value);
}
@@ -136,7 +144,18 @@ public T to(String tailApiUrl, Class type, String method) throws IOExcept
uc.setRequestProperty("Authorization", "Basic " + root.encodedAuthorization);
}
}
- uc.setRequestMethod(method);
+ try {
+ uc.setRequestMethod(method);
+ } catch (ProtocolException e) {
+ // JDK only allows one of the fixed set of verbs. Try to override that
+ try {
+ Field $method = HttpURLConnection.class.getDeclaredField("method");
+ $method.setAccessible(true);
+ $method.set(uc,method);
+ } catch (Exception x) {
+ throw (IOException)new IOException("Failed to set the custom verb").initCause(x);
+ }
+ }
if (v==ApiVersion.V2) {
diff --git a/src/test/java/org/kohsuke/AppTest.java b/src/test/java/org/kohsuke/AppTest.java
index aabee36d41..1e016e771c 100644
--- a/src/test/java/org/kohsuke/AppTest.java
+++ b/src/test/java/org/kohsuke/AppTest.java
@@ -1,6 +1,9 @@
package org.kohsuke;
import junit.framework.TestCase;
+import org.kohsuke.github.GHCommit;
+import org.kohsuke.github.GHCommit.File;
+import org.kohsuke.github.GHCommitComment;
import org.kohsuke.github.GHEvent;
import org.kohsuke.github.GHEventInfo;
import org.kohsuke.github.GHEventPayload;
@@ -14,9 +17,11 @@
import org.kohsuke.github.GHTeam;
import org.kohsuke.github.GHUser;
import org.kohsuke.github.GitHub;
+import org.kohsuke.github.PagedIterable;
import java.io.IOException;
import java.net.URL;
+import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.List;
@@ -74,13 +79,58 @@ public void testMemberOrgs() throws Exception {
System.out.println(o);
}
+ public void testCommit() throws Exception {
+ GitHub gitHub = GitHub.connect();
+ GHCommit commit = gitHub.getUser("jenkinsci").getRepository("jenkins").getCommit("08c1c9970af4d609ae754fbe803e06186e3206f7");
+ System.out.println(commit);
+ assertEquals(1, commit.getParents().size());
+ assertEquals(1,commit.getFiles().size());
+
+ File f = commit.getFiles().get(0);
+ assertEquals(48,f.getLinesChanged());
+ assertEquals("modified",f.getStatus());
+ assertEquals("changelog.html",f.getFileName());
+ }
+
+ public void testListCommits() throws Exception {
+ GitHub gitHub = GitHub.connect();
+ List sha1 = new ArrayList();
+ for (GHCommit c : gitHub.getUser("kohsuke").getRepository("empty-commit").listCommits()) {
+ System.out.println(c.getSHA1());
+ sha1.add(c.getSHA1());
+ }
+ assertEquals("fdfad6be4db6f96faea1f153fb447b479a7a9cb7",sha1.get(0));
+ assertEquals(1,sha1.size());
+ }
+
public void testBranches() throws Exception {
GitHub gitHub = GitHub.connect();
Map b =
gitHub.getUser("jenkinsci").getRepository("jenkins").getBranches();
System.out.println(b);
}
-
+
+ public void testCommitComment() throws Exception {
+ GitHub gitHub = GitHub.connect();
+ GHRepository r = gitHub.getUser("jenkinsci").getRepository("jenkins");
+ PagedIterable comments = r.listCommitComments();
+ List batch = comments.iterator().nextPage();
+ for (GHCommitComment comment : batch) {
+ System.out.println(comment.getBody());
+ assertSame(comment.getOwner(), r);
+ }
+ }
+
+ public void tryCreateCommitComment() throws Exception {
+ GitHub gitHub = GitHub.connect();
+ GHCommit commit = gitHub.getUser("kohsuke").getRepository("sandbox-ant").getCommit("8ae38db0ea5837313ab5f39d43a6f73de3bd9000");
+ GHCommitComment c = commit.createComment("[testing](http://kohsuse.org/)");
+ System.out.println(c);
+ c.update("updated text");
+ System.out.println(c);
+ c.delete();
+ }
+
public void tryHook() throws Exception {
GitHub gitHub = GitHub.connect();
GHRepository r = gitHub.getMyself().getRepository("test2");