diff --git a/pom.xml b/pom.xml index 733dd51a97..fffadeffd9 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.88 + 1.89 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.88 + github-api-1.89 @@ -111,6 +111,12 @@ 4.11 test + + org.hamcrest + hamcrest-all + 1.3 + test + com.fasterxml.jackson.core jackson-databind @@ -144,6 +150,12 @@ 2.7.5 true + + com.squareup.okhttp3 + okhttp-urlconnection + 3.4.0 + true + org.kohsuke wordnet-random-name diff --git a/src/main/java/org/kohsuke/github/GHAuthorization.java b/src/main/java/org/kohsuke/github/GHAuthorization.java index 2a9fa39092..bf7e24c7f2 100644 --- a/src/main/java/org/kohsuke/github/GHAuthorization.java +++ b/src/main/java/org/kohsuke/github/GHAuthorization.java @@ -1,9 +1,9 @@ package org.kohsuke.github; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.net.URL; import java.util.Collection; -import java.util.Date; import java.util.List; /** diff --git a/src/main/java/org/kohsuke/github/GHBlobBuilder.java b/src/main/java/org/kohsuke/github/GHBlobBuilder.java new file mode 100644 index 0000000000..a6259e5b6d --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHBlobBuilder.java @@ -0,0 +1,49 @@ +package org.kohsuke.github; + +import org.apache.commons.codec.binary.Base64; + +import java.io.IOException; + +/** + * Builder pattern for creating a new blob. + * Based on https://developer.github.com/v3/git/blobs/#create-a-blob + */ +public class GHBlobBuilder { + private final GHRepository repo; + private final Requester req; + + GHBlobBuilder(GHRepository repo) { + this.repo = repo; + req = new Requester(repo.root); + } + + /** + * Configures a blob with the specified text {@code content}. + */ + public GHBlobBuilder textContent(String content) { + req.with("content", content); + req.with("encoding", "utf-8"); + return this; + } + + /** + * Configures a blob with the specified binary {@code content}. + */ + public GHBlobBuilder binaryContent(byte[] content) { + String base64Content = Base64.encodeBase64String(content); + req.with("content", base64Content); + req.with("encoding", "base64"); + return this; + } + + private String getApiTail() { + return String.format("/repos/%s/%s/git/blobs", repo.getOwnerName(), repo.getName()); + } + + /** + * Creates a blob based on the parameters specified thus far. + */ + public GHBlob create() throws IOException { + return req.method("POST").to(getApiTail(), GHBlob.class); + } +} diff --git a/src/main/java/org/kohsuke/github/GHBranch.java b/src/main/java/org/kohsuke/github/GHBranch.java index c2e5b29441..1ce9c29ac4 100644 --- a/src/main/java/org/kohsuke/github/GHBranch.java +++ b/src/main/java/org/kohsuke/github/GHBranch.java @@ -1,14 +1,13 @@ package org.kohsuke.github; -import static org.kohsuke.github.Previews.LOKI; +import com.fasterxml.jackson.annotation.JsonProperty; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.net.URL; import java.util.Collection; -import com.fasterxml.jackson.annotation.JsonProperty; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import static org.kohsuke.github.Previews.*; /** * A branch in a repository. diff --git a/src/main/java/org/kohsuke/github/GHBranchProtection.java b/src/main/java/org/kohsuke/github/GHBranchProtection.java index 3216366115..7e43bd69cb 100644 --- a/src/main/java/org/kohsuke/github/GHBranchProtection.java +++ b/src/main/java/org/kohsuke/github/GHBranchProtection.java @@ -1,11 +1,10 @@ package org.kohsuke.github; -import java.util.Collection; - import com.fasterxml.jackson.annotation.JsonProperty; - import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Collection; + @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD" }, justification = "JSON API") public class GHBranchProtection { diff --git a/src/main/java/org/kohsuke/github/GHBranchProtectionBuilder.java b/src/main/java/org/kohsuke/github/GHBranchProtectionBuilder.java index 5fccf7eb8c..dc7dbd8845 100644 --- a/src/main/java/org/kohsuke/github/GHBranchProtectionBuilder.java +++ b/src/main/java/org/kohsuke/github/GHBranchProtectionBuilder.java @@ -1,6 +1,6 @@ package org.kohsuke.github; -import static org.kohsuke.github.Previews.LOKI; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.util.ArrayList; @@ -12,7 +12,7 @@ import java.util.Map; import java.util.Set; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import static org.kohsuke.github.Previews.*; /** * Builder to configure the branch protection settings. diff --git a/src/main/java/org/kohsuke/github/GHCommit.java b/src/main/java/org/kohsuke/github/GHCommit.java index ec5dfbffc1..a817da2fd6 100644 --- a/src/main/java/org/kohsuke/github/GHCommit.java +++ b/src/main/java/org/kohsuke/github/GHCommit.java @@ -2,6 +2,7 @@ import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.io.IOException; import java.net.URL; import java.util.AbstractList; diff --git a/src/main/java/org/kohsuke/github/GHCommitBuilder.java b/src/main/java/org/kohsuke/github/GHCommitBuilder.java new file mode 100644 index 0000000000..76e846a7d7 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHCommitBuilder.java @@ -0,0 +1,92 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +/** + * Builder pattern for creating a new commit. + * Based on https://developer.github.com/v3/git/commits/#create-a-commit + */ +public class GHCommitBuilder { + private final GHRepository repo; + private final Requester req; + + private final List parents = new ArrayList(); + + private static final class UserInfo { + private final String name; + private final String email; + private final String date; + + private UserInfo(String name, String email, Date date) { + this.name = name; + this.email = email; + TimeZone tz = TimeZone.getTimeZone("UTC"); + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + df.setTimeZone(tz); + this.date = df.format((date != null) ? date : new Date()); + } + } + + GHCommitBuilder(GHRepository repo) { + this.repo = repo; + req = new Requester(repo.root); + } + + /** + * @param message the commit message + */ + public GHCommitBuilder message(String message) { + req.with("message", message); + return this; + } + + /** + * @param tree the SHA of the tree object this commit points to + */ + public GHCommitBuilder tree(String tree) { + req.with("tree", tree); + return this; + } + + /** + * @param parent the SHA of a parent commit. + */ + public GHCommitBuilder parent(String parent) { + parents.add(parent); + return this; + } + + /** + * Configures the author of this commit. + */ + public GHCommitBuilder author(String name, String email, Date date) { + req._with("author", new UserInfo(name, email, date)); + return this; + } + + /** + * Configures the committer of this commit. + */ + public GHCommitBuilder committer(String name, String email, Date date) { + req._with("committer", new UserInfo(name, email, date)); + return this; + } + + private String getApiTail() { + return String.format("/repos/%s/%s/git/commits", repo.getOwnerName(), repo.getName()); + } + + /** + * Creates a blob based on the parameters specified thus far. + */ + public GHCommit create() throws IOException { + req._with("parents", parents); + return req.method("POST").to(getApiTail(), GHCommit.class).wrapUp(repo); + } +} diff --git a/src/main/java/org/kohsuke/github/GHCommitComment.java b/src/main/java/org/kohsuke/github/GHCommitComment.java index 3543b5fdb3..f5ee76e478 100644 --- a/src/main/java/org/kohsuke/github/GHCommitComment.java +++ b/src/main/java/org/kohsuke/github/GHCommitComment.java @@ -1,11 +1,11 @@ package org.kohsuke.github; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.io.IOException; import java.net.URL; -import java.util.Date; -import static org.kohsuke.github.Previews.SQUIRREL_GIRL; +import static org.kohsuke.github.Previews.*; /** * A comment attached to a commit (or a specific line in a specific file of a commit.) diff --git a/src/main/java/org/kohsuke/github/GHCommitPointer.java b/src/main/java/org/kohsuke/github/GHCommitPointer.java index b9716366b5..b6c347864e 100644 --- a/src/main/java/org/kohsuke/github/GHCommitPointer.java +++ b/src/main/java/org/kohsuke/github/GHCommitPointer.java @@ -39,7 +39,8 @@ public class GHCommitPointer { * This points to the user who owns * the {@link #getRepository()}. */ - public GHUser getUser() { + public GHUser getUser() throws IOException { + if (user != null) return user.root.intern(user); return user; } diff --git a/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java b/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java index 65136dc0a4..23960528f2 100644 --- a/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java @@ -1,9 +1,9 @@ package org.kohsuke.github; -import java.io.IOException; - import org.apache.commons.lang.StringUtils; +import java.io.IOException; + /** * Search commits. * diff --git a/src/main/java/org/kohsuke/github/GHCommitStatus.java b/src/main/java/org/kohsuke/github/GHCommitStatus.java index 7b876fc5a0..63a62ae8d0 100644 --- a/src/main/java/org/kohsuke/github/GHCommitStatus.java +++ b/src/main/java/org/kohsuke/github/GHCommitStatus.java @@ -1,5 +1,6 @@ package org.kohsuke.github; +import java.io.IOException; import java.net.URL; /** @@ -45,8 +46,8 @@ public String getDescription() { return description; } - public GHUser getCreator() { - return creator; + public GHUser getCreator() throws IOException { + return root.intern(creator); } public String getContext() { diff --git a/src/main/java/org/kohsuke/github/GHContent.java b/src/main/java/org/kohsuke/github/GHContent.java index 8b6f6fea54..d7a7bd446f 100644 --- a/src/main/java/org/kohsuke/github/GHContent.java +++ b/src/main/java/org/kohsuke/github/GHContent.java @@ -7,8 +7,6 @@ import java.io.IOException; import java.io.InputStream; -import javax.xml.bind.DatatypeConverter; - /** * A Content of a repository. * diff --git a/src/main/java/org/kohsuke/github/GHDeployKey.java b/src/main/java/org/kohsuke/github/GHDeployKey.java index eb2c2c00fb..bfa03b2508 100644 --- a/src/main/java/org/kohsuke/github/GHDeployKey.java +++ b/src/main/java/org/kohsuke/github/GHDeployKey.java @@ -1,9 +1,9 @@ package org.kohsuke.github; -import java.io.IOException; - import org.apache.commons.lang.builder.ToStringBuilder; +import java.io.IOException; + public class GHDeployKey { protected String url, key, title; diff --git a/src/main/java/org/kohsuke/github/GHDeployment.java b/src/main/java/org/kohsuke/github/GHDeployment.java index 51a7843d0b..0cef10ee52 100644 --- a/src/main/java/org/kohsuke/github/GHDeployment.java +++ b/src/main/java/org/kohsuke/github/GHDeployment.java @@ -1,6 +1,6 @@ package org.kohsuke.github; - +import java.io.IOException; import java.net.URL; public class GHDeployment extends GHObject { @@ -41,8 +41,8 @@ public String getPayload() { public String getEnvironment() { return environment; } - public GHUser getCreator() { - return creator; + public GHUser getCreator() throws IOException { + return root.intern(creator); } public String getRef() { return ref; diff --git a/src/main/java/org/kohsuke/github/GHDeploymentStatusBuilder.java b/src/main/java/org/kohsuke/github/GHDeploymentStatusBuilder.java index 18cf5464ac..f028b6f889 100644 --- a/src/main/java/org/kohsuke/github/GHDeploymentStatusBuilder.java +++ b/src/main/java/org/kohsuke/github/GHDeploymentStatusBuilder.java @@ -1,7 +1,6 @@ package org.kohsuke.github; import java.io.IOException; -import java.util.Locale; public class GHDeploymentStatusBuilder { private final Requester builder; diff --git a/src/main/java/org/kohsuke/github/GHEventInfo.java b/src/main/java/org/kohsuke/github/GHEventInfo.java index 0268924ea2..e875f2df59 100644 --- a/src/main/java/org/kohsuke/github/GHEventInfo.java +++ b/src/main/java/org/kohsuke/github/GHEventInfo.java @@ -1,11 +1,11 @@ package org.kohsuke.github; -import java.io.IOException; -import java.util.Date; - import com.fasterxml.jackson.databind.node.ObjectNode; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.IOException; +import java.util.Date; + /** * Represents an event. * diff --git a/src/main/java/org/kohsuke/github/GHEventPayload.java b/src/main/java/org/kohsuke/github/GHEventPayload.java index 29f69bb3eb..3d23973fd5 100644 --- a/src/main/java/org/kohsuke/github/GHEventPayload.java +++ b/src/main/java/org/kohsuke/github/GHEventPayload.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSetter; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.io.Reader; import java.util.List; @@ -77,7 +78,7 @@ void wrapUp(GitHub root) { throw new IllegalStateException("Expected pull_request payload, but got something else. Maybe we've got another type of event?"); if (repository!=null) { repository.wrap(root); - pull_request.wrap(repository); + pull_request.wrapUp(repository); } else { pull_request.wrapUp(root); } diff --git a/src/main/java/org/kohsuke/github/GHFileNotFoundException.java b/src/main/java/org/kohsuke/github/GHFileNotFoundException.java new file mode 100644 index 0000000000..9f4b37c560 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHFileNotFoundException.java @@ -0,0 +1,34 @@ +package org.kohsuke.github; + +import javax.annotation.CheckForNull; +import java.io.FileNotFoundException; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; + +/** + * Request/responce contains useful metadata. + * Custom exception allows store info for next diagnostics. + * + * @author Kanstantsin Shautsou + */ +public class GHFileNotFoundException extends FileNotFoundException { + protected Map> responseHeaderFields; + + public GHFileNotFoundException() { + } + + public GHFileNotFoundException(String s) { + super(s); + } + + @CheckForNull + public Map> getResponseHeaderFields() { + return responseHeaderFields; + } + + GHFileNotFoundException withResponseHeaderFields(HttpURLConnection urlConnection) { + this.responseHeaderFields = urlConnection.getHeaderFields(); + return this; + } +} diff --git a/src/main/java/org/kohsuke/github/GHGist.java b/src/main/java/org/kohsuke/github/GHGist.java index eb105b40c6..a9bf3fef42 100644 --- a/src/main/java/org/kohsuke/github/GHGist.java +++ b/src/main/java/org/kohsuke/github/GHGist.java @@ -38,8 +38,8 @@ public class GHGist extends GHObject { /** * User that owns this Gist. */ - public GHUser getOwner() { - return owner; + public GHUser getOwner() throws IOException { + return root.intern(owner); } public String getForksUrl() { diff --git a/src/main/java/org/kohsuke/github/GHHook.java b/src/main/java/org/kohsuke/github/GHHook.java index 4aa32e1946..b7f450307a 100644 --- a/src/main/java/org/kohsuke/github/GHHook.java +++ b/src/main/java/org/kohsuke/github/GHHook.java @@ -1,6 +1,7 @@ package org.kohsuke.github; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.io.IOException; import java.net.URL; import java.util.Collections; diff --git a/src/main/java/org/kohsuke/github/GHHooks.java b/src/main/java/org/kohsuke/github/GHHooks.java index 6f64587b17..6ecbb04b6e 100644 --- a/src/main/java/org/kohsuke/github/GHHooks.java +++ b/src/main/java/org/kohsuke/github/GHHooks.java @@ -5,7 +5,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.Map; /** diff --git a/src/main/java/org/kohsuke/github/GHIOException.java b/src/main/java/org/kohsuke/github/GHIOException.java new file mode 100644 index 0000000000..b07144bcb7 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHIOException.java @@ -0,0 +1,34 @@ +package org.kohsuke.github; + +import javax.annotation.CheckForNull; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Map; + +/** + * Request/responce contains useful metadata. + * Custom exception allows store info for next diagnostics. + * + * @author Kanstantsin Shautsou + */ +public class GHIOException extends IOException { + protected Map> responseHeaderFields; + + public GHIOException() { + } + + public GHIOException(String message) { + super(message); + } + + @CheckForNull + public Map> getResponseHeaderFields() { + return responseHeaderFields; + } + + GHIOException withResponseHeaderFields(HttpURLConnection urlConnection) { + this.responseHeaderFields = urlConnection.getHeaderFields(); + return this; + } +} diff --git a/src/main/java/org/kohsuke/github/GHIssue.java b/src/main/java/org/kohsuke/github/GHIssue.java index 849d4831ff..6b416ca1d3 100644 --- a/src/main/java/org/kohsuke/github/GHIssue.java +++ b/src/main/java/org/kohsuke/github/GHIssue.java @@ -288,8 +288,8 @@ protected String getIssuesApiRoute() { return "/repos/"+owner.getOwnerName()+"/"+owner.getName()+"/issues/"+number; } - public GHUser getAssignee() { - return assignee; + public GHUser getAssignee() throws IOException { + return root.intern(assignee); } public List getAssignees() { @@ -299,8 +299,8 @@ public List getAssignees() { /** * User who submitted the issue. */ - public GHUser getUser() { - return user; + public GHUser getUser() throws IOException { + return root.intern(user); } /** @@ -311,12 +311,16 @@ public GHUser getUser() { * even for an issue that's already closed. See * https://github.com/kohsuke/github-api/issues/60. */ - public GHUser getClosedBy() { + public GHUser getClosedBy() throws IOException { if(!"closed".equals(state)) return null; - if(closed_by != null) return closed_by; - - //TODO closed_by = owner.getIssue(number).getClosed_by(); - return closed_by; + + //TODO + /* + if (closed_by==null) { + closed_by = owner.getIssue(number).getClosed_by(); + } + */ + return root.intern(closed_by); } public int getCommentsCount(){ diff --git a/src/main/java/org/kohsuke/github/GHIssueComment.java b/src/main/java/org/kohsuke/github/GHIssueComment.java index a98f6f6398..3f4d70643d 100644 --- a/src/main/java/org/kohsuke/github/GHIssueComment.java +++ b/src/main/java/org/kohsuke/github/GHIssueComment.java @@ -26,7 +26,7 @@ import java.io.IOException; import java.net.URL; -import static org.kohsuke.github.Previews.SQUIRREL_GIRL; +import static org.kohsuke.github.Previews.*; /** * Comment to the issue diff --git a/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java b/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java index f9634a59f1..6cefc01678 100644 --- a/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java @@ -1,7 +1,5 @@ package org.kohsuke.github; -import java.util.Locale; - /** * Search issues. * diff --git a/src/main/java/org/kohsuke/github/GHLicense.java b/src/main/java/org/kohsuke/github/GHLicense.java index 2c5c4299c1..a7dad4b5ca 100644 --- a/src/main/java/org/kohsuke/github/GHLicense.java +++ b/src/main/java/org/kohsuke/github/GHLicense.java @@ -32,7 +32,7 @@ import java.util.ArrayList; import java.util.List; -import static org.kohsuke.github.Previews.DRAX; +import static org.kohsuke.github.Previews.*; /** * The GitHub Preview API's license information diff --git a/src/main/java/org/kohsuke/github/GHMilestone.java b/src/main/java/org/kohsuke/github/GHMilestone.java index 48f8364bbf..50ad549e82 100644 --- a/src/main/java/org/kohsuke/github/GHMilestone.java +++ b/src/main/java/org/kohsuke/github/GHMilestone.java @@ -27,8 +27,8 @@ public GHRepository getOwner() { return owner; } - public GHUser getCreator() { - return creator; + public GHUser getCreator() throws IOException { + return root.intern(creator); } public Date getDueOn() { diff --git a/src/main/java/org/kohsuke/github/GHMyself.java b/src/main/java/org/kohsuke/github/GHMyself.java index cc05f03bc5..5ab9b4a270 100644 --- a/src/main/java/org/kohsuke/github/GHMyself.java +++ b/src/main/java/org/kohsuke/github/GHMyself.java @@ -6,7 +6,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; diff --git a/src/main/java/org/kohsuke/github/GHObject.java b/src/main/java/org/kohsuke/github/GHObject.java index a81d913a5c..96e41a2559 100644 --- a/src/main/java/org/kohsuke/github/GHObject.java +++ b/src/main/java/org/kohsuke/github/GHObject.java @@ -3,14 +3,15 @@ import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.commons.lang.builder.ReflectionToStringBuilder; -import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; -import org.apache.commons.lang.reflect.FieldUtils; +import javax.annotation.CheckForNull; import java.io.IOException; import java.lang.reflect.Field; import java.net.URL; import java.util.Date; +import java.util.List; +import java.util.Map; /** * Most (all?) domain objects in GitHub seems to have these 4 properties. @@ -18,6 +19,11 @@ @SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD"}, justification = "JSON API") public abstract class GHObject { + /** + * Capture response HTTP headers on the state object. + */ + protected Map> responseHeaderFields; + protected String url; protected int id; protected String created_at; @@ -26,6 +32,21 @@ public abstract class GHObject { /*package*/ GHObject() { } + /** + * Returns the HTTP response headers given along with the state of this object. + * + *

+ * Some of the HTTP headers have nothing to do with the object, for example "Cache-Control" + * and others are different depending on how this object was retrieved. + * + * This method was added as a kind of hack to allow the caller to retrieve OAuth scopes and such. + * Use with caution. The method might be removed in the future. + */ + @CheckForNull @Deprecated + public Map> getResponseHeaderFields() { + return responseHeaderFields; + } + /** * When was this resource created? */ diff --git a/src/main/java/org/kohsuke/github/GHPullRequest.java b/src/main/java/org/kohsuke/github/GHPullRequest.java index 078603d6fa..6e1a5d90e5 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequest.java +++ b/src/main/java/org/kohsuke/github/GHPullRequest.java @@ -23,6 +23,7 @@ */ package org.kohsuke.github; +import javax.annotation.CheckForNull; import java.io.IOException; import java.net.URL; import java.util.ArrayList; @@ -30,9 +31,8 @@ import java.util.Collection; import java.util.Date; import java.util.List; -import javax.annotation.CheckForNull; -import static org.kohsuke.github.Previews.BLACK_CAT; +import static org.kohsuke.github.Previews.*; /** * A pull request. @@ -206,7 +206,7 @@ public String getMergeCommitSha() throws IOException { * Depending on the original API call where this object is created, it may not contain everything. */ private void populate() throws IOException { - if (merged_by!=null) return; // already populated + if (mergeable_state!=null) return; // already populated if (root.isOffline()) { return; // cannot populate, will have to live with what we have } @@ -346,9 +346,29 @@ public void merge(String msg) throws IOException { * SHA that pull request head must match to allow merge. */ public void merge(String msg, String sha) throws IOException { - new Requester(root).method("PUT").with("commit_message",msg).with("sha",sha).to(getApiRoute()+"/merge"); + merge(msg, sha, null); } + /** + * Merge this pull request, using the specified merge method. + * + * The equivalent of the big green "Merge pull request" button. + * + * @param msg + * Commit message. If null, the default one will be used. + * @param method + * SHA that pull request head must match to allow merge. + */ + public void merge(String msg, String sha, MergeMethod method) throws IOException { + new Requester(root).method("PUT") + .with("commit_message",msg) + .with("sha",sha) + .with("merge_method",method) + .to(getApiRoute()+"/merge"); + } + + public enum MergeMethod{ MERGE, SQUASH, REBASE } + private void fetchIssue() throws IOException { if (!fetchedIssueDetails) { new Requester(root).to(getIssuesApiRoute(), this); diff --git a/src/main/java/org/kohsuke/github/GHPullRequestReview.java b/src/main/java/org/kohsuke/github/GHPullRequestReview.java index b25f28b59a..d7afe59aad 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequestReview.java +++ b/src/main/java/org/kohsuke/github/GHPullRequestReview.java @@ -26,7 +26,7 @@ import java.io.IOException; import java.net.URL; -import static org.kohsuke.github.Previews.BLACK_CAT; +import static org.kohsuke.github.Previews.*; /** * Review to the pull request diff --git a/src/main/java/org/kohsuke/github/GHRateLimit.java b/src/main/java/org/kohsuke/github/GHRateLimit.java index b9b900d785..9500b0a736 100644 --- a/src/main/java/org/kohsuke/github/GHRateLimit.java +++ b/src/main/java/org/kohsuke/github/GHRateLimit.java @@ -1,6 +1,7 @@ package org.kohsuke.github; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.util.Date; /** diff --git a/src/main/java/org/kohsuke/github/GHReaction.java b/src/main/java/org/kohsuke/github/GHReaction.java index 55b26365e0..6a00eb305c 100644 --- a/src/main/java/org/kohsuke/github/GHReaction.java +++ b/src/main/java/org/kohsuke/github/GHReaction.java @@ -3,7 +3,7 @@ import java.io.IOException; import java.net.URL; -import static org.kohsuke.github.Previews.SQUIRREL_GIRL; +import static org.kohsuke.github.Previews.*; /** * Reaction to issue, comment, PR, and so on. diff --git a/src/main/java/org/kohsuke/github/GHRef.java b/src/main/java/org/kohsuke/github/GHRef.java index 8212dc2c3b..c8462d3fe6 100644 --- a/src/main/java/org/kohsuke/github/GHRef.java +++ b/src/main/java/org/kohsuke/github/GHRef.java @@ -1,6 +1,7 @@ package org.kohsuke.github; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.io.IOException; import java.net.URL; diff --git a/src/main/java/org/kohsuke/github/GHRelease.java b/src/main/java/org/kohsuke/github/GHRelease.java index 96421da415..a299add2c5 100644 --- a/src/main/java/org/kohsuke/github/GHRelease.java +++ b/src/main/java/org/kohsuke/github/GHRelease.java @@ -8,7 +8,7 @@ import java.util.Date; import java.util.List; -import static java.lang.String.format; +import static java.lang.String.*; /** * Release in a github repository. @@ -45,10 +45,12 @@ public boolean isDraft() { return draft; } + /** + * @deprecated + * Use {@link #update()} + */ public GHRelease setDraft(boolean draft) throws IOException { - edit("draft", draft); - this.draft = draft; - return this; + return update().draft(draft).update(); } public URL getHtmlUrl() { @@ -149,10 +151,10 @@ public void delete() throws IOException { } /** - * Edit this release. + * Updates this release via a builder. */ - private void edit(String key, Object value) throws IOException { - new Requester(root)._with(key, value).method("PATCH").to(owner.getApiTailUrl("releases/"+id)); + public GHReleaseUpdater update() { + return new GHReleaseUpdater(this); } private String getApiTailUrl(String end) { diff --git a/src/main/java/org/kohsuke/github/GHReleaseBuilder.java b/src/main/java/org/kohsuke/github/GHReleaseBuilder.java index b1daac956e..e427bdf8a5 100644 --- a/src/main/java/org/kohsuke/github/GHReleaseBuilder.java +++ b/src/main/java/org/kohsuke/github/GHReleaseBuilder.java @@ -21,9 +21,7 @@ public GHReleaseBuilder(GHRepository ghRepository, String tag) { * @param body The release notes body. */ public GHReleaseBuilder body(String body) { - if (body != null) { - builder.with("body", body); - } + builder.with("body", body); return this; } @@ -35,9 +33,7 @@ public GHReleaseBuilder body(String body) { * already exists. */ public GHReleaseBuilder commitish(String commitish) { - if (commitish != null) { - builder.with("target_commitish", commitish); - } + builder.with("target_commitish", commitish); return this; } @@ -56,9 +52,7 @@ public GHReleaseBuilder draft(boolean draft) { * @param name the name of the release */ public GHReleaseBuilder name(String name) { - if (name != null) { - builder.with("name", name); - } + builder.with("name", name); return this; } diff --git a/src/main/java/org/kohsuke/github/GHReleaseUpdater.java b/src/main/java/org/kohsuke/github/GHReleaseUpdater.java new file mode 100644 index 0000000000..a34a5b0bc2 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHReleaseUpdater.java @@ -0,0 +1,81 @@ +package org.kohsuke.github; + +import java.io.IOException; + +/** + * Modifies {@link GHRelease}. + * + * @author Kohsuke Kawaguchi + * @see GHRelease#update() + */ +public class GHReleaseUpdater { + private final GHRelease base; + private final Requester builder; + + GHReleaseUpdater(GHRelease base) { + this.base = base; + this.builder = new Requester(base.root); + } + + public GHReleaseUpdater tag(String tag) { + builder.with("tag_name",tag); + return this; + } + + /** + * @param body The release notes body. + */ + public GHReleaseUpdater body(String body) { + builder.with("body", body); + return this; + } + + /** + * Specifies the commitish value that determines where the Git tag is created from. Can be any branch or + * commit SHA. + * + * @param commitish Defaults to the repository’s default branch (usually "master"). Unused if the Git tag + * already exists. + */ + public GHReleaseUpdater commitish(String commitish) { + builder.with("target_commitish", commitish); + return this; + } + + /** + * Optional. + * + * @param draft {@code true} to create a draft (unpublished) release, {@code false} to create a published one. + * Default is {@code false}. + */ + public GHReleaseUpdater draft(boolean draft) { + builder.with("draft", draft); + return this; + } + + /** + * @param name the name of the release + */ + public GHReleaseUpdater name(String name) { + builder.with("name", name); + return this; + } + + /** + * Optional + * + * @param prerelease {@code true} to identify the release as a prerelease. {@code false} to identify the release + * as a full release. Default is {@code false}. + */ + public GHReleaseUpdater prerelease(boolean prerelease) { + builder.with("prerelease", prerelease); + return this; + } + + public GHRelease update() throws IOException { + return builder + .method("PATCH") + .to(base.owner.getApiTailUrl("releases/"+base.id), GHRelease.class).wrap(base.owner); + } + +} diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index d40549fd78..29cac80d44 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -883,6 +883,10 @@ public GHTree getTree(String sha) throws IOException { return root.retrieve().to(url, GHTree.class).wrap(this); } + public GHTreeBuilder createTree() { + return new GHTreeBuilder(this); + } + /** * Retrieves the tree for the current GitHub repository, recursively as described in here: * https://developer.github.com/v3/git/trees/#get-a-tree-recursively @@ -912,6 +916,10 @@ public GHBlob getBlob(String blobSha) throws IOException { return root.retrieve().to(target, GHBlob.class); } + public GHBlobBuilder createBlob() { + return new GHBlobBuilder(this); + } + /** * Reads the content of a blob as a stream for better efficiency. * @@ -935,6 +943,10 @@ public GHCommit getCommit(String sha1) throws IOException { return c; } + public GHCommitBuilder createCommit() { + return new GHCommitBuilder(this); + } + /** * Lists all the commits. */ @@ -1540,6 +1552,19 @@ public GHNotificationStream listNotifications() { return new GHNotificationStream(root,getApiTailUrl("/notifications")); } + /** + * https://developer.github.com/v3/repos/traffic/#views + */ + public GHRepositoryViewTraffic getViewTraffic() throws IOException{ + return root.retrieve().to(getApiTailUrl("/traffic/views"), GHRepositoryViewTraffic.class); + } + + /** + * https://developer.github.com/v3/repos/traffic/#clones + */ + public GHRepositoryCloneTraffic getCloneTraffic() throws IOException{ + return root.retrieve().to(getApiTailUrl("/traffic/clones"), GHRepositoryCloneTraffic.class); + } @Override public int hashCode() { diff --git a/src/main/java/org/kohsuke/github/GHRepositoryCloneTraffic.java b/src/main/java/org/kohsuke/github/GHRepositoryCloneTraffic.java new file mode 100644 index 0000000000..c75198e4a8 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHRepositoryCloneTraffic.java @@ -0,0 +1,37 @@ +package org.kohsuke.github; + +import java.util.List; + +/** + * Repository clone statistics. + * + * @see GHRepository#getCloneTraffic() + */ +public class GHRepositoryCloneTraffic extends GHRepositoryTraffic { + private List clones; + + /*package*/ GHRepositoryCloneTraffic() { + } + + /*package*/ GHRepositoryCloneTraffic(Integer count, Integer uniques, List clones) { + super(count, uniques); + this.clones = clones; + } + + public List getClones() { + return clones; + } + + public List getDailyInfo() { + return getClones(); + } + + public static class DailyInfo extends GHRepositoryTraffic.DailyInfo { + /*package*/ DailyInfo() { + } + + /*package*/ DailyInfo(String timestamp, int count, int uniques) { + super(timestamp, count, uniques); + } + } +} diff --git a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java index 03b12f5133..642f7f3d95 100644 --- a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java @@ -1,7 +1,5 @@ package org.kohsuke.github; -import java.util.Locale; - /** * Search repositories. * diff --git a/src/main/java/org/kohsuke/github/GHRepositoryTraffic.java b/src/main/java/org/kohsuke/github/GHRepositoryTraffic.java new file mode 100644 index 0000000000..42d07e848d --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHRepositoryTraffic.java @@ -0,0 +1,54 @@ +package org.kohsuke.github; + +import java.util.Date; +import java.util.List; + +public abstract class GHRepositoryTraffic implements TrafficInfo { + private int count; + private int uniques; + + /*package*/ GHRepositoryTraffic() { + } + + /*package*/ GHRepositoryTraffic(int count, int uniques) { + this.count = count; + this.uniques = uniques; + } + + public int getCount() { + return count; + } + + public int getUniques() { + return uniques; + } + + public abstract List getDailyInfo(); + + public static abstract class DailyInfo implements TrafficInfo { + private String timestamp; + private int count; + private int uniques; + + public Date getTimestamp() { + return GitHub.parseDate(timestamp); + } + + public int getCount() { + return count; + } + + public int getUniques() { + return uniques; + } + + /*package*/ DailyInfo() { + } + + /*package*/ DailyInfo(String timestamp, Integer count, Integer uniques) { + this.timestamp = timestamp; + this.count = count; + this.uniques = uniques; + } + } +} diff --git a/src/main/java/org/kohsuke/github/GHRepositoryViewTraffic.java b/src/main/java/org/kohsuke/github/GHRepositoryViewTraffic.java new file mode 100644 index 0000000000..f2f1e5b440 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHRepositoryViewTraffic.java @@ -0,0 +1,37 @@ +package org.kohsuke.github; + +import java.util.List; + +/** + * Repository view statistics. + * + * @see GHRepository#getViewTraffic() + */ +public class GHRepositoryViewTraffic extends GHRepositoryTraffic { + private List views; + + /*package*/ GHRepositoryViewTraffic() { + } + + /*package*/ GHRepositoryViewTraffic(int count, int uniques, List views) { + super(count, uniques); + this.views = views; + } + + public List getViews() { + return views; + } + + public List getDailyInfo() { + return getViews(); + } + + public static class DailyInfo extends GHRepositoryTraffic.DailyInfo { + /*package*/ DailyInfo() { + } + + /*package*/ DailyInfo(String timestamp, int count, int uniques) { + super(timestamp, count, uniques); + } + } +} diff --git a/src/main/java/org/kohsuke/github/GHThread.java b/src/main/java/org/kohsuke/github/GHThread.java index a1964b10de..faf4c87b7b 100644 --- a/src/main/java/org/kohsuke/github/GHThread.java +++ b/src/main/java/org/kohsuke/github/GHThread.java @@ -1,6 +1,7 @@ package org.kohsuke.github; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.io.FileNotFoundException; import java.io.IOException; import java.net.URL; diff --git a/src/main/java/org/kohsuke/github/GHTreeBuilder.java b/src/main/java/org/kohsuke/github/GHTreeBuilder.java new file mode 100644 index 0000000000..122c5775b5 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHTreeBuilder.java @@ -0,0 +1,90 @@ +package org.kohsuke.github; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Builder pattern for creating a new tree. + * Based on https://developer.github.com/v3/git/trees/#create-a-tree + */ +public class GHTreeBuilder { + private final GHRepository repo; + private final Requester req; + + private final List treeEntries = new ArrayList(); + + @SuppressFBWarnings("URF_UNREAD_FIELD") + private static final class TreeEntry { + private final String path; + private final String mode; + private final String type; + private String sha; + private String content; + + private TreeEntry(String path, String mode, String type) { + this.path = path; + this.mode = mode; + this.type = type; + } + } + + GHTreeBuilder(GHRepository repo) { + this.repo = repo; + req = new Requester(repo.root); + } + + /** + * @param baseTree the SHA of tree you want to update with new data + */ + public GHTreeBuilder baseTree(String baseTree) { + req.with("base_tree", baseTree); + return this; + } + + /** + * Adds a new entry to the tree. + * Exactly one of the parameters {@code sha} and {@code content} must be non-null. + */ + public GHTreeBuilder entry(String path, String mode, String type, String sha, String content) { + TreeEntry entry = new TreeEntry(path, mode, type); + entry.sha = sha; + entry.content = content; + treeEntries.add(entry); + return this; + } + + /** + * Specialized version of {@link #entry(String, String, String, String, String)} for adding an existing blob referred by its SHA. + */ + public GHTreeBuilder shaEntry(String path, String sha, boolean executable) { + TreeEntry entry = new TreeEntry(path, executable ? "100755" : "100644", "blob"); + entry.sha = sha; + treeEntries.add(entry); + return this; + } + + /** + * Specialized version of {@link #entry(String, String, String, String, String)} for adding a text file with the specified {@code content}. + */ + public GHTreeBuilder textEntry(String path, String content, boolean executable) { + TreeEntry entry = new TreeEntry(path, executable ? "100755" : "100644", "blob"); + entry.content = content; + treeEntries.add(entry); + return this; + } + + private String getApiTail() { + return String.format("/repos/%s/%s/git/trees", repo.getOwnerName(), repo.getName()); + } + + /** + * Creates a tree based on the parameters specified thus far. + */ + public GHTree create() throws IOException { + req._with("tree", treeEntries); + return req.method("POST").to(getApiTail(), GHTree.class).wrap(repo); + } +} diff --git a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java index ca40f4d050..3edcd15065 100644 --- a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java @@ -1,7 +1,5 @@ package org.kohsuke.github; -import java.util.Locale; - /** * Search users. * diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 49c860e4dc..62c1b4e80c 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -27,6 +27,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.introspect.VisibilityChecker.Std; import com.infradna.tool.bridge_method_injector.WithBridgeMethods; +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -47,18 +53,14 @@ import java.util.Map; import java.util.Set; import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.logging.Logger; -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; -import org.apache.commons.codec.Charsets; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.IOUtils; -import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; -import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; -import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; -import static java.util.logging.Level.FINE; -import static org.kohsuke.github.Previews.DRAX; +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.*; +import static java.net.HttpURLConnection.*; +import static java.util.logging.Level.*; +import static org.kohsuke.github.Previews.*; /** * Root of the GitHub API. @@ -79,9 +81,10 @@ public class GitHub { */ /*package*/ final String encodedAuthorization; - private final Map users = new Hashtable(); - private final Map orgs = new Hashtable(); - + private final ConcurrentMap users; + private final ConcurrentMap orgs; + // Cache of myself object. + private GHMyself myself; private final String apiUrl; /*package*/ final RateLimitHandler rateLimitHandler; @@ -146,6 +149,8 @@ public class GitHub { } } + users = new ConcurrentHashMap(); + orgs = new ConcurrentHashMap(); this.rateLimitHandler = rateLimitHandler; this.abuseLimitHandler = abuseLimitHandler; @@ -357,13 +362,15 @@ public GHRateLimit rateLimit() throws IOException { @WithBridgeMethods(GHUser.class) public GHMyself getMyself() throws IOException { requireCredential(); + synchronized (this) { + if (this.myself != null) return myself; + + GHMyself u = retrieve().to("/user", GHMyself.class); - GHMyself u = retrieve().to("/user", GHMyself.class); - - u.root = this; - users.put(u.getLogin(), u); - - return u; + u.root = this; + this.myself = u; + return u; + } } /** @@ -379,7 +386,7 @@ public GHUser getUser(String login) throws IOException { return u; } - + /** * clears all cached data in order for external changes (modifications and del */ @@ -641,6 +648,18 @@ public boolean isCredentialValid() throws IOException { } } + /*package*/ GHUser intern(GHUser user) throws IOException { + if (user==null) return user; + + // if we already have this user in our map, use it + GHUser u = users.get(user.getLogin()); + if (u!=null) return u; + + // if not, remember this new user + users.putIfAbsent(user.getLogin(),user); + return user; + } + private static class GHApiInfo { private String rate_limit_url; diff --git a/src/main/java/org/kohsuke/github/GitUser.java b/src/main/java/org/kohsuke/github/GitUser.java index 751aabd378..9cd50bb22a 100644 --- a/src/main/java/org/kohsuke/github/GitUser.java +++ b/src/main/java/org/kohsuke/github/GitUser.java @@ -1,6 +1,7 @@ package org.kohsuke.github; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + import java.util.Date; /** diff --git a/src/main/java/org/kohsuke/github/HttpConnector.java b/src/main/java/org/kohsuke/github/HttpConnector.java index 72f7ce5b05..2d87c148c9 100644 --- a/src/main/java/org/kohsuke/github/HttpConnector.java +++ b/src/main/java/org/kohsuke/github/HttpConnector.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; -import java.util.concurrent.TimeUnit; /** * Pluggability for customizing HTTP request behaviors or using altogether different library. diff --git a/src/main/java/org/kohsuke/github/HttpException.java b/src/main/java/org/kohsuke/github/HttpException.java index 16c8e68be2..79def83c2a 100644 --- a/src/main/java/org/kohsuke/github/HttpException.java +++ b/src/main/java/org/kohsuke/github/HttpException.java @@ -1,11 +1,10 @@ package org.kohsuke.github; +import javax.annotation.CheckForNull; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; -import javax.annotation.CheckForNull; - /** * {@link IOException} for http exceptions because {@link HttpURLConnection} throws un-discerned * {@link IOException} and it can help to know the http response code to decide how to handle an diff --git a/src/main/java/org/kohsuke/github/PagedIterable.java b/src/main/java/org/kohsuke/github/PagedIterable.java index 41c5bfdb0c..1c6eccea36 100644 --- a/src/main/java/org/kohsuke/github/PagedIterable.java +++ b/src/main/java/org/kohsuke/github/PagedIterable.java @@ -1,6 +1,5 @@ package org.kohsuke.github; -import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; diff --git a/src/main/java/org/kohsuke/github/PagedSearchIterable.java b/src/main/java/org/kohsuke/github/PagedSearchIterable.java index 1efe49a1ef..f23bd6b58c 100644 --- a/src/main/java/org/kohsuke/github/PagedSearchIterable.java +++ b/src/main/java/org/kohsuke/github/PagedSearchIterable.java @@ -2,7 +2,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.IOException; import java.util.Iterator; /** diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 576d313f5d..00f11f2426 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -25,6 +25,11 @@ import com.fasterxml.jackson.databind.JsonMappingException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; + +import javax.annotation.CheckForNull; +import javax.annotation.WillClose; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -49,18 +54,16 @@ import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; +import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; -import javax.annotation.WillClose; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; -import static java.util.Arrays.asList; -import java.util.logging.Level; +import static java.util.Arrays.*; import static java.util.logging.Level.*; -import static org.kohsuke.github.GitHub.MAPPER; +import static org.apache.commons.lang.StringUtils.*; +import static org.kohsuke.github.GitHub.*; /** * A builder pattern for making HTTP call and parsing its output. @@ -76,7 +79,7 @@ class Requester { * Request method. */ private String method = "POST"; - private String contentType = "application/x-www-form-urlencoded"; + private String contentType = null; private InputStream body; /** @@ -275,7 +278,7 @@ private T _to(String tailApiUrl, Class type, T instance) throws IOExcepti if (nextLinkMatcher.find()) { final String link = nextLinkMatcher.group(1); T nextResult = _to(link, type, instance); - + setResponseHeaders(nextResult); final int resultLength = Array.getLength(result); final int nextResultLength = Array.getLength(nextResult); T concatResult = (T) Array.newInstance(type.getComponentType(), resultLength + nextResultLength); @@ -285,7 +288,7 @@ private T _to(String tailApiUrl, Class type, T instance) throws IOExcepti } } } - return result; + return setResponseHeaders(result); } catch (IOException e) { handleApiError(e); } finally { @@ -392,18 +395,19 @@ public String getResponseHeader(String header) { private void buildRequest() throws IOException { if (isMethodWithBody()) { uc.setDoOutput(true); - uc.setRequestProperty("Content-type", contentType); if (body == null) { + uc.setRequestProperty("Content-type", defaultString(contentType,"application/json")); Map json = new HashMap(); for (Entry e : args) { json.put(e.key, e.value); } MAPPER.writeValue(uc.getOutputStream(), json); } else { + uc.setRequestProperty("Content-type", defaultString(contentType,"application/x-www-form-urlencoded")); try { byte[] bytes = new byte[32768]; - int read = 0; + int read; while ((read = body.read(bytes)) != -1) { uc.getOutputStream().write(bytes, 0, read); } @@ -585,6 +589,7 @@ private void setRequestMethod(HttpURLConnection uc) throws IOException { throw new IllegalStateException("Failed to set the request method to "+method); } + @CheckForNull private T parse(Class type, T instance) throws IOException { return parse(type, instance, 2); } @@ -608,12 +613,13 @@ private T parse(Class type, T instance, int timeouts) throws IOException String data = IOUtils.toString(r); if (type!=null) try { - return MAPPER.readValue(data,type); + return setResponseHeaders(MAPPER.readValue(data, type)); } catch (JsonMappingException e) { throw (IOException)new IOException("Failed to deserialize " +data).initCause(e); } - if (instance!=null) - return MAPPER.readerForUpdating(instance).readValue(data); + if (instance!=null) { + return setResponseHeaders(MAPPER.readerForUpdating(instance).readValue(data)); + } return null; } catch (FileNotFoundException e) { // java.net.URLConnection handles 404 exception has FileNotFoundException, don't wrap exception in HttpException @@ -621,7 +627,7 @@ private T parse(Class type, T instance, int timeouts) throws IOException throw e; } catch (IOException e) { if (e instanceof SocketTimeoutException && timeouts > 0) { - LOGGER.log(Level.INFO, "timed out accessing " + uc.getURL() + "; will try " + timeouts + " more time(s)", e); + LOGGER.log(INFO, "timed out accessing " + uc.getURL() + "; will try " + timeouts + " more time(s)", e); return parse(type, instance, timeouts - 1); } throw new HttpException(responseCode, responseMessage, uc.getURL(), e); @@ -630,6 +636,21 @@ private T parse(Class type, T instance, int timeouts) throws IOException } } + private T setResponseHeaders(T readValue) { + if (readValue instanceof GHObject[]) { + for (GHObject ghObject : (GHObject[]) readValue) { + setResponseHeaders(ghObject); + } + } else if (readValue instanceof GHObject) { + setResponseHeaders((GHObject) readValue); + } + return readValue; + } + + private void setResponseHeaders(GHObject readValue) { + readValue.responseHeaderFields = uc.getHeaderFields(); + } + /** * Handles the "Content-Encoding" header. */ @@ -662,13 +683,13 @@ private InputStream wrapStream(InputStream in) throws IOException { String error = IOUtils.toString(es, "UTF-8"); if (e instanceof FileNotFoundException) { // pass through 404 Not Found to allow the caller to handle it intelligently - e = (IOException) new FileNotFoundException(error).initCause(e); + e = (IOException) new GHFileNotFoundException(error).withResponseHeaderFields(uc).initCause(e); } else if (e instanceof HttpException) { HttpException http = (HttpException) e; e = new HttpException(error, http.getResponseCode(), http.getResponseMessage(), http.getUrl(), e); } else { - e = (IOException) new IOException(error).initCause(e); + e = (IOException) new GHIOException(error).withResponseHeaderFields(uc).initCause(e); } } finally { IOUtils.closeQuietly(es); diff --git a/src/main/java/org/kohsuke/github/TrafficInfo.java b/src/main/java/org/kohsuke/github/TrafficInfo.java new file mode 100644 index 0000000000..9b232e9f1a --- /dev/null +++ b/src/main/java/org/kohsuke/github/TrafficInfo.java @@ -0,0 +1,16 @@ +package org.kohsuke.github; + +/** + * @author Kohsuke Kawaguchi + */ +public interface TrafficInfo { + /** + * Total count of hits. + */ + int getCount(); + + /** + * Unique visitors. + */ + int getUniques(); +} diff --git a/src/main/java/org/kohsuke/github/extras/OkHttp3Connector.java b/src/main/java/org/kohsuke/github/extras/OkHttp3Connector.java new file mode 100644 index 0000000000..d2fd8c6978 --- /dev/null +++ b/src/main/java/org/kohsuke/github/extras/OkHttp3Connector.java @@ -0,0 +1,32 @@ +package org.kohsuke.github.extras; + +import okhttp3.OkHttpClient; +import okhttp3.OkUrlFactory; +import org.kohsuke.github.HttpConnector; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * {@link HttpConnector} for {@link OkHttpClient}. + * + * Unlike {@link #DEFAULT}, OkHttp does response caching. + * Making a conditional request against GitHubAPI and receiving a 304 + * response does not count against the rate limit. + * See http://developer.github.com/v3/#conditional-requests + * + * @author Roberto Tyley + * @author Kohsuke Kawaguchi + */ +public class OkHttp3Connector implements HttpConnector { + private final OkUrlFactory urlFactory; + + public OkHttp3Connector(OkUrlFactory urlFactory) { + this.urlFactory = urlFactory; + } + + public HttpURLConnection connect(URL url) throws IOException { + return urlFactory.open(url); + } +} diff --git a/src/test/java/Foo.java b/src/test/java/Foo.java index 35e5eb5955..540f9fd3ca 100644 --- a/src/test/java/Foo.java +++ b/src/test/java/Foo.java @@ -1,10 +1,7 @@ -import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHRepository.Contributor; import org.kohsuke.github.GHUser; import org.kohsuke.github.GitHub; -import java.util.Collection; - /** * @author Kohsuke Kawaguchi */ diff --git a/src/test/java/org/kohsuke/github/AppTest.java b/src/test/java/org/kohsuke/github/AppTest.java index 248bd0ef11..7087920dd7 100755 --- a/src/test/java/org/kohsuke/github/AppTest.java +++ b/src/test/java/org/kohsuke/github/AppTest.java @@ -5,8 +5,6 @@ import com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; -import org.hamcrest.CoreMatchers; -import org.junit.Assume; import org.junit.Test; import org.kohsuke.github.GHCommit.File; import org.kohsuke.github.GHOrganization.Permission; @@ -16,7 +14,6 @@ import java.net.URL; import java.util.*; import java.util.Map.Entry; -import java.util.concurrent.ExecutionException; import java.util.regex.Pattern; import static org.hamcrest.CoreMatchers.*; diff --git a/src/test/java/org/kohsuke/github/CommitTest.java b/src/test/java/org/kohsuke/github/CommitTest.java index c42fceef41..8e4edb1fed 100644 --- a/src/test/java/org/kohsuke/github/CommitTest.java +++ b/src/test/java/org/kohsuke/github/CommitTest.java @@ -1,7 +1,6 @@ package org.kohsuke.github; import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; import org.junit.Test; import java.io.IOException; diff --git a/src/test/java/org/kohsuke/github/GHHookTest.java b/src/test/java/org/kohsuke/github/GHHookTest.java new file mode 100644 index 0000000000..b27484b5e5 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GHHookTest.java @@ -0,0 +1,78 @@ +package org.kohsuke.github; + +import org.apache.commons.lang.StringUtils; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasValue; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertThat; + + +/** + * @author Kanstantsin Shautsou + */ +public class GHHookTest { + + @Ignore + @Test + public void exposeResponceHeaders() throws Exception { + String user1Login = "KostyaSha-auto"; + String user1Pass = "secret"; + + String clientId = "90140219451"; + String clientSecret = "1451245425"; + + String orgRepo = "KostyaSha-org/test"; + + // some login based user that has access to application + final GitHub gitHub = GitHub.connectUsingPassword(user1Login, user1Pass); + gitHub.getMyself(); + + // we request read + final List scopes = Arrays.asList("repo", "read:org", "user:email", "read:repo_hook"); + + // application creates token with scopes + final GHAuthorization auth = gitHub.createOrGetAuth(clientId, clientSecret, scopes, "", ""); + String token = auth.getToken(); + if (StringUtils.isEmpty(token)) { + gitHub.deleteAuth(auth.getId()); + token = gitHub.createOrGetAuth(clientId, clientSecret, scopes, "", "").getToken(); + } + + /// now create connection using token + final GitHub gitHub2 = GitHub.connectUsingOAuth(token); + // some repo in organisation + final GHRepository repository = gitHub2.getRepository(orgRepo); + + // doesn't fail because we have read access + final List hooks = repository.getHooks(); + + try { + // fails because application isn't approved in organisation and you can find it only after doing real call + final GHHook hook = repository.createHook( + "my-hook", + singletonMap("url", "http://localhost"), + singletonList(GHEvent.PUSH), + true + ); + } catch (IOException ex) { + assertThat(ex, instanceOf(GHFileNotFoundException.class)); + final GHFileNotFoundException ghFileNotFoundException = (GHFileNotFoundException) ex; + final Map> responseHeaderFields = ghFileNotFoundException.getResponseHeaderFields(); + assertThat(responseHeaderFields, hasKey("X-Accepted-OAuth-Scopes")); + assertThat(responseHeaderFields.get("X-Accepted-OAuth-Scopes"), + hasItem("admin:repo_hook, public_repo, repo, write:repo_hook") + ); + } + } +} diff --git a/src/test/java/org/kohsuke/github/GitHubTest.java b/src/test/java/org/kohsuke/github/GitHubTest.java index 010adc8b24..a418db5fde 100644 --- a/src/test/java/org/kohsuke/github/GitHubTest.java +++ b/src/test/java/org/kohsuke/github/GitHubTest.java @@ -8,7 +8,6 @@ import java.util.Map; import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; import org.junit.Test; import static org.hamcrest.CoreMatchers.notNullValue; diff --git a/src/test/java/org/kohsuke/github/PullRequestTest.java b/src/test/java/org/kohsuke/github/PullRequestTest.java index ce09b2ab13..5ea2628303 100644 --- a/src/test/java/org/kohsuke/github/PullRequestTest.java +++ b/src/test/java/org/kohsuke/github/PullRequestTest.java @@ -99,6 +99,19 @@ public void testMergeCommitSHA() throws Exception { fail(); } + @Test + public void testSquashMerge() throws Exception { + String name = rnd.next(); + GHRef masterRef = getRepository().getRef("heads/master"); + GHRef branchRef = getRepository().createRef("refs/heads/" + name, masterRef.getObject().getSha()); + getRepository().createContent(name, name, name, name); + Thread.sleep(1000); + GHPullRequest p = getRepository().createPullRequest(name, name, "master", "## test squash"); + Thread.sleep(1000); + p.merge("squash merge", null, GHPullRequest.MergeMethod.SQUASH); + branchRef.delete(); + } + @Test // Requires push access to the test repo to pass public void setLabels() throws Exception { diff --git a/src/test/java/org/kohsuke/github/RepositoryTrafficTest.java b/src/test/java/org/kohsuke/github/RepositoryTrafficTest.java new file mode 100644 index 0000000000..b1f13e6c97 --- /dev/null +++ b/src/test/java/org/kohsuke/github/RepositoryTrafficTest.java @@ -0,0 +1,167 @@ +package org.kohsuke.github; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.junit.Assert; +import org.junit.Test; +import org.kohsuke.github.GHRepositoryTraffic.DailyInfo; +import org.mockito.Mockito; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.TimeZone; + +public class RepositoryTrafficTest { + final private String login = "kohsuke", repositoryName = "github-api"; + + @SuppressWarnings("unchecked") + private void checkResponse(T expected, T actual){ + Assert.assertEquals(expected.getCount(), actual.getCount()); + Assert.assertEquals(expected.getUniques(), actual.getUniques()); + + List expectedList = expected.getDailyInfo(); + List actualList = actual.getDailyInfo(); + Iterator expectedIt; + Iterator actualIt; + + Assert.assertEquals(expectedList.size(), actualList.size()); + expectedIt = expectedList.iterator(); + actualIt = actualList.iterator(); + + while(expectedIt.hasNext() && actualIt.hasNext()) { + DailyInfo expectedDailyInfo = expectedIt.next(); + DailyInfo actualDailyInfo = actualIt.next(); + Assert.assertEquals(expectedDailyInfo.getCount(), actualDailyInfo.getCount()); + Assert.assertEquals(expectedDailyInfo.getUniques(), actualDailyInfo.getUniques()); + Assert.assertEquals(expectedDailyInfo.getTimestamp(), actualDailyInfo.getTimestamp()); + } + } + + private void testTraffic(T expectedResult) throws IOException{ + SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + ObjectMapper mapper = new ObjectMapper().setDateFormat(dateFormat); + String mockedResponse = mapper.writeValueAsString(expectedResult); + + + GitHub gitHub = GitHub.connect(login, null); + GitHub gitHubSpy = Mockito.spy(gitHub); + GHRepository repo = gitHubSpy.getUser(login).getRepository(repositoryName); + + + // accessing traffic info requires push access to the repo + // since we don't have that, let the mocking begin... + + HttpConnector connectorSpy = Mockito.spy(gitHubSpy.getConnector()); + Mockito.doReturn(connectorSpy).when(gitHubSpy).getConnector(); + + + // also known as the "uc" in the Requester class + HttpURLConnection mockHttpURLConnection = Mockito.mock(HttpURLConnection.class); + + + // needed for Requester.setRequestMethod + Mockito.doReturn("GET").when(mockHttpURLConnection).getRequestMethod(); + + + // this covers calls on "uc" in Requester.setupConnection and Requester.buildRequest + URL trafficURL = new URL( + "https://api.github.com/repos/"+login+"/"+repositoryName+"/traffic/" + + ((expectedResult instanceof GHRepositoryViewTraffic) ? "views" : "clones") + ); + Mockito.doReturn(mockHttpURLConnection).when(connectorSpy).connect(Mockito.eq(trafficURL)); + + + // make Requester.parse work + Mockito.doReturn(200).when(mockHttpURLConnection).getResponseCode(); + Mockito.doReturn("OK").when(mockHttpURLConnection).getResponseMessage(); + InputStream stubInputStream = IOUtils.toInputStream(mockedResponse, "UTF-8"); + Mockito.doReturn(stubInputStream).when(mockHttpURLConnection).getInputStream(); + + if(expectedResult instanceof GHRepositoryViewTraffic){ + GHRepositoryViewTraffic views = repo.getViewTraffic(); + checkResponse(expectedResult, views); + } + else if(expectedResult instanceof GHRepositoryCloneTraffic) { + GHRepositoryCloneTraffic clones = repo.getCloneTraffic(); + checkResponse(expectedResult, clones); + } + } + + @Test + public void testGetViews() throws IOException{ + GHRepositoryViewTraffic expectedResult = new GHRepositoryViewTraffic( + 21523359, + 65534, + Arrays.asList( + new GHRepositoryViewTraffic.DailyInfo("2016-10-10T00:00:00Z", 3, 2), + new GHRepositoryViewTraffic.DailyInfo("2016-10-11T00:00:00Z", 9, 4), + new GHRepositoryViewTraffic.DailyInfo("2016-10-12T00:00:00Z", 27, 8), + new GHRepositoryViewTraffic.DailyInfo("2016-10-13T00:00:00Z", 81, 16), + new GHRepositoryViewTraffic.DailyInfo("2016-10-14T00:00:00Z", 243, 32), + new GHRepositoryViewTraffic.DailyInfo("2016-10-15T00:00:00Z", 729, 64), + new GHRepositoryViewTraffic.DailyInfo("2016-10-16T00:00:00Z", 2187, 128), + new GHRepositoryViewTraffic.DailyInfo("2016-10-17T00:00:00Z", 6561, 256), + new GHRepositoryViewTraffic.DailyInfo("2016-10-18T00:00:00Z", 19683, 512), + new GHRepositoryViewTraffic.DailyInfo("2016-10-19T00:00:00Z", 59049, 1024), + new GHRepositoryViewTraffic.DailyInfo("2016-10-20T00:00:00Z", 177147, 2048), + new GHRepositoryViewTraffic.DailyInfo("2016-10-21T00:00:00Z", 531441, 4096), + new GHRepositoryViewTraffic.DailyInfo("2016-10-22T00:00:00Z", 1594323, 8192), + new GHRepositoryViewTraffic.DailyInfo("2016-10-23T00:00:00Z", 4782969, 16384), + new GHRepositoryViewTraffic.DailyInfo("2016-10-24T00:00:00Z", 14348907, 32768) + ) + ); + testTraffic(expectedResult); + } + + @Test + public void testGetClones() throws IOException{ + GHRepositoryCloneTraffic expectedResult = new GHRepositoryCloneTraffic( + 1500, + 455, + Arrays.asList( + new GHRepositoryCloneTraffic.DailyInfo("2016-10-10T00:00:00Z", 10,3), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-11T00:00:00Z", 20,6), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-12T00:00:00Z", 30,5), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-13T00:00:00Z", 40,7), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-14T00:00:00Z", 50,11), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-15T00:00:00Z", 60,12), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-16T00:00:00Z", 70,19), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-17T00:00:00Z", 170,111), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-18T00:00:00Z", 180,70), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-19T00:00:00Z", 190,10), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-20T00:00:00Z", 200,18), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-21T00:00:00Z", 210,8), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-22T00:00:00Z", 220,168), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-23T00:00:00Z", 5,2), + new GHRepositoryCloneTraffic.DailyInfo("2016-10-24T00:00:00Z", 45,5) + ) + ); + testTraffic(expectedResult); + } + + @Test + public void testGetTrafficStatsAccessFailureDueToInsufficientPermissions() throws IOException { + String errorMsg = "Exception should be thrown, since we don't have permission to access repo traffic info."; + GitHub gitHub = GitHub.connect(login, null); + GHRepository repo = gitHub.getUser(login).getRepository(repositoryName); + try { + repo.getViewTraffic(); + Assert.fail(errorMsg); + } + catch (HttpException ex){ + } + try { + repo.getCloneTraffic(); + Assert.fail(errorMsg); + } + catch (HttpException ex){ + } + } +}