diff --git a/.gitignore b/.gitignore index cb9b4d6a4a..3c2c3c6427 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ target *.iml *.ipr *.iws +.classpath +.project +.settings/ +.DS_Store diff --git a/pom.xml b/pom.xml index 4d623c86fa..7e204af06d 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ github-api - 1.86 + 1.87 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.86 + github-api-1.87 @@ -34,6 +34,12 @@ + + maven-surefire-plugin + + 2 + + org.codehaus.mojo animal-sniffer-maven-plugin diff --git a/src/main/java/org/kohsuke/github/BranchProtection.java b/src/main/java/org/kohsuke/github/BranchProtection.java deleted file mode 100644 index 4ae03b162a..0000000000 --- a/src/main/java/org/kohsuke/github/BranchProtection.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.kohsuke.github; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -import java.util.ArrayList; -import java.util.List; - -/** - * @author Kohsuke Kawaguchi - * @see GHBranch#disableProtection() - */ -@SuppressFBWarnings(value = {"UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD"}, justification = "JSON API") -class BranchProtection { - boolean enabled; - RequiredStatusChecks requiredStatusChecks; - - static class RequiredStatusChecks { - EnforcementLevel enforcement_level; - List contexts = new ArrayList(); - } -} diff --git a/src/main/java/org/kohsuke/github/EnforcementLevel.java b/src/main/java/org/kohsuke/github/EnforcementLevel.java index 1b29195299..81c86428c4 100644 --- a/src/main/java/org/kohsuke/github/EnforcementLevel.java +++ b/src/main/java/org/kohsuke/github/EnforcementLevel.java @@ -3,8 +3,11 @@ import java.util.Locale; /** + * This was added during preview API period but it has changed since then. + * * @author Kohsuke Kawaguchi */ +@Deprecated public enum EnforcementLevel { OFF, NON_ADMINS, EVERYONE; diff --git a/src/main/java/org/kohsuke/github/GHBranch.java b/src/main/java/org/kohsuke/github/GHBranch.java index f54bea9f47..c2e5b29441 100644 --- a/src/main/java/org/kohsuke/github/GHBranch.java +++ b/src/main/java/org/kohsuke/github/GHBranch.java @@ -4,21 +4,18 @@ import java.io.IOException; import java.net.URL; -import java.util.Arrays; import java.util.Collection; -import org.kohsuke.github.BranchProtection.RequiredStatusChecks; - import com.fasterxml.jackson.annotation.JsonProperty; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * A branch in a repository. - * + * * @author Yusuke Kokubo */ -@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", +@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD"}, justification = "JSON API") public class GHBranch { private GitHub root; @@ -33,7 +30,7 @@ public class GHBranch { public static class Commit { String sha; - + @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "We don't provide it in API now") String url; } @@ -69,6 +66,10 @@ public URL getProtectionUrl() { return GitHub.parseURL(protection_url); } + @Preview @Deprecated + public GHBranchProtection getProtection() throws IOException { + return root.retrieve().withPreview(LOKI).to(protection_url, GHBranchProtection.class); + } /** * The commit that this branch currently points to. @@ -82,9 +83,7 @@ public String getSHA1() { */ @Preview @Deprecated public void disableProtection() throws IOException { - BranchProtection bp = new BranchProtection(); - bp.enabled = false; - setProtection(bp); + new Requester(root).method("DELETE").withPreview(LOKI).to(protection_url); } /** @@ -93,28 +92,31 @@ public void disableProtection() throws IOException { * @see GHCommitStatus#getContext() */ @Preview @Deprecated - public void enableProtection(EnforcementLevel level, Collection contexts) throws IOException { - BranchProtection bp = new BranchProtection(); - bp.enabled = true; - bp.requiredStatusChecks = new RequiredStatusChecks(); - bp.requiredStatusChecks.enforcement_level = level; - bp.requiredStatusChecks.contexts.addAll(contexts); - setProtection(bp); - } - - @Preview @Deprecated - public void enableProtection(EnforcementLevel level, String... contexts) throws IOException { - enableProtection(level, Arrays.asList(contexts)); + public GHBranchProtectionBuilder enableProtection() { + return new GHBranchProtectionBuilder(this); } - private void setProtection(BranchProtection bp) throws IOException { - new Requester(root).method("PATCH").withPreview(LOKI)._with("protection",bp).to(getApiRoute()); + // backward compatibility with previous signature + @Deprecated + public void enableProtection(EnforcementLevel level, Collection contexts) throws IOException { + switch (level) { + case OFF: + disableProtection(); + break; + case NON_ADMINS: + case EVERYONE: + enableProtection() + .addRequiredChecks(contexts) + .includeAdmins(level==EnforcementLevel.EVERYONE) + .enable(); + break; + } } String getApiRoute() { return owner.getApiTailUrl("/branches/"+name); } - + @Override public String toString() { final String url = owner != null ? owner.getUrl().toString() : "unknown"; diff --git a/src/main/java/org/kohsuke/github/GHBranchProtection.java b/src/main/java/org/kohsuke/github/GHBranchProtection.java new file mode 100644 index 0000000000..3216366115 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHBranchProtection.java @@ -0,0 +1,152 @@ +package org.kohsuke.github; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +@SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", + "URF_UNREAD_FIELD" }, justification = "JSON API") +public class GHBranchProtection { + @JsonProperty("enforce_admins") + private EnforceAdmins enforceAdmins; + + @JsonProperty("required_pull_request_reviews") + private RequiredReviews requiredReviews; + + @JsonProperty("required_status_checks") + private RequiredStatusChecks requiredStatusChecks; + + @JsonProperty + private Restrictions restrictions; + + @JsonProperty + private String url; + + public EnforceAdmins getEnforceAdmins() { + return enforceAdmins; + } + + public RequiredReviews getRequiredReviews() { + return requiredReviews; + } + + public RequiredStatusChecks getRequiredStatusChecks() { + return requiredStatusChecks; + } + + public Restrictions getRestrictions() { + return restrictions; + } + + public String getUrl() { + return url; + } + + public static class EnforceAdmins { + @JsonProperty + private boolean enabled; + + @JsonProperty + private String url; + + public String getUrl() { + return url; + } + + public boolean isEnabled() { + return enabled; + } + } + + public static class RequiredReviews { + @JsonProperty("dismissal_restrictions") + private Restrictions dismissalRestriction; + + @JsonProperty("dismiss_stale_reviews") + private boolean dismissStaleReviews; + + @JsonProperty("require_code_owner_reviews") + private boolean requireCodeOwnerReviews; + + @JsonProperty + private String url; + + public Restrictions getDismissalRestrictions() { + return dismissalRestriction; + } + + public String getUrl() { + return url; + } + + public boolean isDismissStaleReviews() { + return dismissStaleReviews; + } + + public boolean isRequireCodeOwnerReviews() { + return requireCodeOwnerReviews; + } + } + + public static class RequiredStatusChecks { + @JsonProperty + private Collection contexts; + + @JsonProperty + private boolean strict; + + @JsonProperty + private String url; + + public Collection getContexts() { + return contexts; + } + + public String getUrl() { + return url; + } + + public boolean isRequiresBranchUpToDate() { + return strict; + } + } + + public static class Restrictions { + @JsonProperty + private Collection teams; + + @JsonProperty("teams_url") + private String teamsUrl; + + @JsonProperty + private String url; + + @JsonProperty + private Collection users; + + @JsonProperty("users_url") + private String usersUrl; + + public Collection getTeams() { + return teams; + } + + public String getTeamsUrl() { + return teamsUrl; + } + + public String getUrl() { + return url; + } + + public Collection getUsers() { + return users; + } + + public String getUsersUrl() { + return usersUrl; + } + } +} diff --git a/src/main/java/org/kohsuke/github/GHBranchProtectionBuilder.java b/src/main/java/org/kohsuke/github/GHBranchProtectionBuilder.java new file mode 100644 index 0000000000..5fccf7eb8c --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHBranchProtectionBuilder.java @@ -0,0 +1,203 @@ +package org.kohsuke.github; + +import static org.kohsuke.github.Previews.LOKI; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Builder to configure the branch protection settings. + * + * @see GHBranch#enableProtection() + */ +@SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", + "URF_UNREAD_FIELD" }, justification = "JSON API") +public class GHBranchProtectionBuilder { + private final GHBranch branch; + + private boolean enforceAdmins; + private Map prReviews; + private Restrictions restrictions; + private StatusChecks statusChecks; + + GHBranchProtectionBuilder(GHBranch branch) { + this.branch = branch; + } + + public GHBranchProtectionBuilder addRequiredChecks(Collection checks) { + getStatusChecks().contexts.addAll(checks); + return this; + } + + public GHBranchProtectionBuilder addRequiredChecks(String... checks) { + addRequiredChecks(Arrays.asList(checks)); + return this; + } + + public GHBranchProtectionBuilder dismissStaleReviews() { + getPrReviews().put("dismiss_stale_reviews", true); + return this; + } + + public GHBranchProtection enable() throws IOException { + return requester().method("PUT") + .withNullable("required_status_checks", statusChecks) + .withNullable("required_pull_request_reviews", prReviews) + .withNullable("restrictions", restrictions) + .withNullable("enforce_admins", enforceAdmins) + .to(branch.getProtectionUrl().toString(), GHBranchProtection.class); + } + + public GHBranchProtectionBuilder includeAdmins() { + return includeAdmins(true); + } + + public GHBranchProtectionBuilder includeAdmins(boolean v) { + enforceAdmins = v; + return this; + } + + public GHBranchProtectionBuilder requireBranchIsUpToDate() { + return requireBranchIsUpToDate(true); + } + + public GHBranchProtectionBuilder requireBranchIsUpToDate(boolean v) { + getStatusChecks().strict = v; + return this; + } + + public GHBranchProtectionBuilder requireCodeOwnReviews() { + return requireCodeOwnReviews(true); + } + + public GHBranchProtectionBuilder requireCodeOwnReviews(boolean v) { + getPrReviews().put("require_code_owner_reviews", v); + return this; + } + + public GHBranchProtectionBuilder requireReviews() { + getPrReviews(); + return this; + } + + public GHBranchProtectionBuilder restrictPushAccess() { + getRestrictions(); + return this; + } + + public GHBranchProtectionBuilder teamPushAccess(Collection teams) { + for (GHTeam team : teams) { + teamPushAccess(team); + } + return this; + } + + public GHBranchProtectionBuilder teamPushAccess(GHTeam... teams) { + for (GHTeam team : teams) { + getRestrictions().teams.add(team.getSlug()); + } + return this; + } + + public GHBranchProtectionBuilder teamReviewDismissals(Collection teams) { + for (GHTeam team : teams) { + teamReviewDismissals(team); + } + return this; + } + + public GHBranchProtectionBuilder teamReviewDismissals(GHTeam... teams) { + for (GHTeam team : teams) { + addReviewRestriction(team.getSlug(), true); + } + return this; + } + + public GHBranchProtectionBuilder userPushAccess(Collection users) { + for (GHUser user : users) { + userPushAccess(user); + } + return this; + } + + public GHBranchProtectionBuilder userPushAccess(GHUser... users) { + for (GHUser user : users) { + getRestrictions().users.add(user.getLogin()); + } + return this; + } + + public GHBranchProtectionBuilder userReviewDismissals(Collection users) { + for (GHUser team : users) { + userReviewDismissals(team); + } + return this; + } + + public GHBranchProtectionBuilder userReviewDismissals(GHUser... users) { + for (GHUser user : users) { + addReviewRestriction(user.getLogin(), false); + } + return this; + } + + private void addReviewRestriction(String restriction, boolean isTeam) { + getPrReviews(); + + if (!prReviews.containsKey("dismissal_restrictions")) { + prReviews.put("dismissal_restrictions", new Restrictions()); + } + + Restrictions restrictions = (Restrictions) prReviews.get("dismissal_restrictions"); + + if (isTeam) { + restrictions.teams.add(restriction); + } else { + restrictions.users.add(restriction); + } + } + + private Map getPrReviews() { + if (prReviews == null) { + prReviews = new HashMap(); + } + return prReviews; + } + + private Restrictions getRestrictions() { + if (restrictions == null) { + restrictions = new Restrictions(); + } + return restrictions; + } + + private StatusChecks getStatusChecks() { + if (statusChecks == null) { + statusChecks = new StatusChecks(); + } + return statusChecks; + } + + private Requester requester() { + return new Requester(branch.getRoot()).withPreview(LOKI); + } + + private static class Restrictions { + private Set teams = new HashSet(); + private Set users = new HashSet(); + } + + private static class StatusChecks { + final List contexts = new ArrayList(); + boolean strict; + } +} diff --git a/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java b/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java new file mode 100644 index 0000000000..65136dc0a4 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java @@ -0,0 +1,137 @@ +package org.kohsuke.github; + +import java.io.IOException; + +import org.apache.commons.lang.StringUtils; + +/** + * Search commits. + * + * @author Marc de Verdelhan + * @see GitHub#searchCommits() + */ +@Preview @Deprecated +public class GHCommitSearchBuilder extends GHSearchBuilder { + /*package*/ GHCommitSearchBuilder(GitHub root) { + super(root,CommitSearchResult.class); + req.withPreview(Previews.CLOAK); + } + + /** + * Search terms. + */ + public GHCommitSearchBuilder q(String term) { + super.q(term); + return this; + } + + public GHCommitSearchBuilder author(String v) { + return q("author:"+v); + } + + public GHCommitSearchBuilder committer(String v) { + return q("committer:"+v); + } + + public GHCommitSearchBuilder authorName(String v) { + return q("author-name:"+v); + } + + public GHCommitSearchBuilder committerName(String v) { + return q("committer-name:"+v); + } + + public GHCommitSearchBuilder authorEmail(String v) { + return q("author-email:"+v); + } + + public GHCommitSearchBuilder committerEmail(String v) { + return q("committer-email:"+v); + } + + public GHCommitSearchBuilder authorDate(String v) { + return q("author-date:"+v); + } + + public GHCommitSearchBuilder committerDate(String v) { + return q("committer-date:"+v); + } + + public GHCommitSearchBuilder merge(boolean merge) { + return q("merge:"+Boolean.valueOf(merge).toString().toLowerCase()); + } + + public GHCommitSearchBuilder hash(String v) { + return q("hash:"+v); + } + + public GHCommitSearchBuilder parent(String v) { + return q("parent:"+v); + } + + public GHCommitSearchBuilder tree(String v) { + return q("tree:"+v); + } + + public GHCommitSearchBuilder is(String v) { + return q("is:"+v); + } + + public GHCommitSearchBuilder user(String v) { + return q("user:"+v); + } + + public GHCommitSearchBuilder org(String v) { + return q("org:"+v); + } + + public GHCommitSearchBuilder repo(String v) { + return q("repo:"+v); + } + + public GHCommitSearchBuilder order(GHDirection v) { + req.with("order",v); + return this; + } + + public GHCommitSearchBuilder sort(Sort sort) { + req.with("sort",sort); + return this; + } + + public enum Sort { AUTHOR_DATE, COMMITTER_DATE } + + private static class CommitSearchResult extends SearchResult { + private GHCommit[] items; + + @Override + /*package*/ GHCommit[] getItems(GitHub root) { + for (GHCommit commit : items) { + String repoName = getRepoName(commit.url); + try { + GHRepository repo = root.getRepository(repoName); + commit.wrapUp(repo); + } catch (IOException ioe) {} + } + return items; + } + } + + /** + * @param commitUrl a commit URL + * @return the repo name ("username/reponame") + */ + private static String getRepoName(String commitUrl) { + if (StringUtils.isBlank(commitUrl)) { + return null; + } + int indexOfUsername = (GitHub.GITHUB_URL + "/repos/").length(); + String[] tokens = commitUrl.substring(indexOfUsername).split("/", 3); + return tokens[0] + '/' + tokens[1]; + } + + @Override + protected String getApiUrl() { + return "/search/commits"; + } +} diff --git a/src/main/java/org/kohsuke/github/GHEvent.java b/src/main/java/org/kohsuke/github/GHEvent.java index b33cb991eb..970b6afbf7 100644 --- a/src/main/java/org/kohsuke/github/GHEvent.java +++ b/src/main/java/org/kohsuke/github/GHEvent.java @@ -21,17 +21,30 @@ public enum GHEvent { FORK_APPLY, GIST, GOLLUM, + INSTALLATION, + INSTALLATION_REPOSITORIES, ISSUE_COMMENT, ISSUES, + LABEL, + MARKETPLACE_PURCHASE, MEMBER, + MEMBERSHIP, + MILESTONE, + ORGANIZATION, + ORG_BLOCK, PAGE_BUILD, + PROJECT_CARD, + PROJECT_COLUMN, + PROJECT, PUBLIC, PULL_REQUEST, + PULL_REQUEST_REVIEW, PULL_REQUEST_REVIEW_COMMENT, PUSH, RELEASE, REPOSITORY, // only valid for org hooks STATUS, + TEAM, TEAM_ADD, WATCH, PING, diff --git a/src/main/java/org/kohsuke/github/GHHook.java b/src/main/java/org/kohsuke/github/GHHook.java index 73b2eb4364..4aa32e1946 100644 --- a/src/main/java/org/kohsuke/github/GHHook.java +++ b/src/main/java/org/kohsuke/github/GHHook.java @@ -41,6 +41,13 @@ public Map getConfig() { return Collections.unmodifiableMap(config); } + /** + * @see Ping hook + */ + public void ping() throws IOException { + new Requester(getRoot()).method("POST").to(getApiRoute() + "/pings"); + } + /** * Deletes this hook. */ diff --git a/src/main/java/org/kohsuke/github/GHPullRequest.java b/src/main/java/org/kohsuke/github/GHPullRequest.java index 04898039bc..078603d6fa 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequest.java +++ b/src/main/java/org/kohsuke/github/GHPullRequest.java @@ -25,8 +25,14 @@ import java.io.IOException; import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.List; +import javax.annotation.CheckForNull; + +import static org.kohsuke.github.Previews.BLACK_CAT; /** * A pull request. @@ -208,7 +214,7 @@ private void populate() throws IOException { } /** - * Retrieves all the commits associated to this pull request. + * Retrieves all the files associated to this pull request. */ public PagedIterable listFiles() { return new PagedIterable() { @@ -223,6 +229,27 @@ protected void wrapUp(GHPullRequestFileDetail[] page) { }; } + /** + * Retrieves all the reviews associated to this pull request. + */ + public PagedIterable listReviews() { + return new PagedIterable() { + public PagedIterator _iterator(int pageSize) { + return new PagedIterator(root.retrieve() + .withPreview(BLACK_CAT) + .asIterator(String.format("%s/reviews", getApiRoute()), + GHPullRequestReview[].class, pageSize)) { + @Override + protected void wrapUp(GHPullRequestReview[] page) { + for (GHPullRequestReview r: page) { + r.wrapUp(GHPullRequest.this); + } + } + }; + } + }; + } + /** * Obtains all the review comments associated with this pull request. */ @@ -259,6 +286,34 @@ protected void wrapUp(GHPullRequestCommitDetail[] page) { }; } + @Preview + @Deprecated + public GHPullRequestReview createReview(String body, @CheckForNull GHPullRequestReviewState event, + GHPullRequestReviewComment... comments) + throws IOException { + return createReview(body, event, Arrays.asList(comments)); + } + + @Preview + @Deprecated + public GHPullRequestReview createReview(String body, @CheckForNull GHPullRequestReviewState event, + List comments) + throws IOException { +// if (event == null) { +// event = GHPullRequestReviewState.PENDING; +// } + List draftComments = new ArrayList(comments.size()); + for (GHPullRequestReviewComment c : comments) { + draftComments.add(new DraftReviewComment(c.getBody(), c.getPath(), c.getPosition())); + } + return new Requester(root).method("POST") + .with("body", body) + //.with("event", event.name()) + ._with("comments", draftComments) + .withPreview(BLACK_CAT) + .to(getApiRoute() + "/reviews", GHPullRequestReview.class).wrapUp(this); + } + public GHPullRequestReviewComment createReviewComment(String body, String sha, String path, int position) throws IOException { return new Requester(root).method("POST") .with("body", body) @@ -300,4 +355,28 @@ private void fetchIssue() throws IOException { fetchedIssueDetails = true; } } + + private static class DraftReviewComment { + private String body; + private String path; + private int position; + + public DraftReviewComment(String body, String path, int position) { + this.body = body; + this.path = path; + this.position = position; + } + + public String getBody() { + return body; + } + + public String getPath() { + return path; + } + + public int getPosition() { + return position; + } + } } diff --git a/src/main/java/org/kohsuke/github/GHPullRequestReview.java b/src/main/java/org/kohsuke/github/GHPullRequestReview.java new file mode 100644 index 0000000000..b25f28b59a --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHPullRequestReview.java @@ -0,0 +1,149 @@ +/* + * The MIT License + * + * Copyright (c) 2017, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.kohsuke.github; + +import java.io.IOException; +import java.net.URL; + +import static org.kohsuke.github.Previews.BLACK_CAT; + +/** + * Review to the pull request + * + * @see GHPullRequest#listReviews() + * @see GHPullRequest#createReview(String, GHPullRequestReviewState, GHPullRequestReviewComment...) + */ +public class GHPullRequestReview extends GHObject { + GHPullRequest owner; + + private String body; + private GHUser user; + private String commit_id; + private GHPullRequestReviewState state; + + /*package*/ GHPullRequestReview wrapUp(GHPullRequest owner) { + this.owner = owner; + return this; + } + + /** + * Gets the pull request to which this review is associated. + */ + public GHPullRequest getParent() { + return owner; + } + + /** + * The comment itself. + */ + public String getBody() { + return body; + } + + /** + * Gets the user who posted this review. + */ + public GHUser getUser() throws IOException { + return owner.root.getUser(user.getLogin()); + } + + public String getCommitId() { + return commit_id; + } + + public GHPullRequestReviewState getState() { + return state; + } + + @Override + public URL getHtmlUrl() { + return null; + } + + protected String getApiRoute() { + return owner.getApiRoute()+"/reviews/"+id; + } + + /** + * Updates the comment. + */ + @Preview + @Deprecated + public void submit(String body, GHPullRequestReviewState event) throws IOException { + new Requester(owner.root).method("POST") + .with("body", body) + .with("event", event.action()) + .withPreview("application/vnd.github.black-cat-preview+json") + .to(getApiRoute()+"/events",this); + this.body = body; + this.state = event; + } + + /** + * Deletes this review. + */ + @Preview + @Deprecated + public void delete() throws IOException { + new Requester(owner.root).method("DELETE") + .withPreview(BLACK_CAT) + .to(getApiRoute()); + } + + /** + * Dismisses this review. + */ + @Preview + @Deprecated + public void dismiss(String message) throws IOException { + new Requester(owner.root).method("PUT") + .with("message", message) + .withPreview(BLACK_CAT) + .to(getApiRoute()+"/dismissals"); + state = GHPullRequestReviewState.DISMISSED; + } + + /** + * Obtains all the review comments associated with this pull request review. + */ + @Preview + @Deprecated + public PagedIterable listReviewComments() throws IOException { + return new PagedIterable() { + public PagedIterator _iterator(int pageSize) { + return new PagedIterator( + owner.root.retrieve() + .withPreview(BLACK_CAT) + .asIterator(getApiRoute() + "/comments", + GHPullRequestReviewComment[].class, pageSize)) { + protected void wrapUp(GHPullRequestReviewComment[] page) { + for (GHPullRequestReviewComment c : page) + c.wrapUp(owner); + } + }; + } + }; + } + +} diff --git a/src/main/java/org/kohsuke/github/GHPullRequestReviewComment.java b/src/main/java/org/kohsuke/github/GHPullRequestReviewComment.java index 05784b5078..33097bc29d 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequestReviewComment.java +++ b/src/main/java/org/kohsuke/github/GHPullRequestReviewComment.java @@ -44,6 +44,14 @@ public class GHPullRequestReviewComment extends GHObject implements Reactable { private int position; private int originalPosition; + public static GHPullRequestReviewComment draft(String body, String path, int position) { + GHPullRequestReviewComment result = new GHPullRequestReviewComment(); + result.body = body; + result.path = path; + result.position = position; + return result; + } + /*package*/ GHPullRequestReviewComment wrapUp(GHPullRequest owner) { this.owner = owner; return this; diff --git a/src/main/java/org/kohsuke/github/GHPullRequestReviewState.java b/src/main/java/org/kohsuke/github/GHPullRequestReviewState.java new file mode 100644 index 0000000000..fca3fabfb3 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHPullRequestReviewState.java @@ -0,0 +1,19 @@ +package org.kohsuke.github; + +public enum GHPullRequestReviewState { + PENDING(null), + APPROVED("APPROVE"), + REQUEST_CHANGES("REQUEST_CHANGES"), + COMMENTED("COMMENT"), + DISMISSED(null); + + private final String _action; + + GHPullRequestReviewState(String action) { + _action = action; + } + + public String action() { + return _action; + } +} diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 74fbb4d665..d40549fd78 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -60,7 +60,7 @@ * @author Kohsuke Kawaguchi */ @SuppressWarnings({"UnusedDeclaration"}) -@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", +@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD"}, justification = "JSON API") public class GHRepository extends GHObject { /*package almost final*/ GitHub root; @@ -298,6 +298,14 @@ public GHRef createRef(String name, String sha) throws IOException { public List getReleases() throws IOException { return listReleases().asList(); } + + public GHRelease getLatestRelease() throws IOException { + try { + return root.retrieve().to(getApiTailUrl("releases/latest"), GHRelease.class).wrap(this); + } catch (FileNotFoundException e) { + return null; // no latest release + } + } public PagedIterable listReleases() throws IOException { return new PagedIterable() { @@ -434,8 +442,8 @@ public String getMasterBranch() { public int getSize() { return size; } - - + + /** * Gets the collaborators on this repository. * This set always appear to include the owner. @@ -486,9 +494,8 @@ public Set getCollaboratorNames() throws IOException { * @throws FileNotFoundException under some conditions (e.g., private repo you can see but are not an admin of); treat as unknown * @throws HttpException with a 403 under other conditions (e.g., public repo you have no special rights to); treat as unknown */ - @Deprecated @Preview public GHPermissionType getPermission(String user) throws IOException { - GHPermission perm = root.retrieve().withPreview(KORRA).to(getApiTailUrl("collaborators/" + user + "/permission"), GHPermission.class); + GHPermission perm = root.retrieve().to(getApiTailUrl("collaborators/" + user + "/permission"), GHPermission.class); perm.wrapUp(root); return perm.getPermissionType(); } @@ -498,7 +505,6 @@ public GHPermissionType getPermission(String user) throws IOException { * @throws FileNotFoundException under some conditions (e.g., private repo you can see but are not an admin of); treat as unknown * @throws HttpException with a 403 under other conditions (e.g., public repo you have no special rights to); treat as unknown */ - @Deprecated @Preview public GHPermissionType getPermission(GHUser u) throws IOException { return getPermission(u.getLogin()); } @@ -784,6 +790,26 @@ public GHRef[] getRefs() throws IOException { return GHRef.wrap(root.retrieve().to(String.format("/repos/%s/%s/git/refs", getOwnerName(), name), GHRef[].class), root); } + + /** + * Retrieves all refs for the github repository. + * + * @return paged iterable of all refs + * @throws IOException on failure communicating with GitHub, potentially due to an invalid ref type being requested + */ + public PagedIterable listRefs() throws IOException { + final String url = String.format("/repos/%s/%s/git/refs", getOwnerName(), name); + return new PagedIterable() { + public PagedIterator _iterator(int pageSize) { + return new PagedIterator(root.retrieve().asIterator(url, GHRef[].class, pageSize)) { + protected void wrapUp(GHRef[] page) { + // no-op + } + }; + } + }; + } + /** * Retrieves all refs of the given type for the current GitHub repository. * @param refType the type of reg to search for e.g. tags or commits @@ -793,6 +819,27 @@ public GHRef[] getRefs() throws IOException { public GHRef[] getRefs(String refType) throws IOException { return GHRef.wrap(root.retrieve().to(String.format("/repos/%s/%s/git/refs/%s", getOwnerName(), name, refType), GHRef[].class),root); } + + /** + * Retrieves all refs of the given type for the current GitHub repository. + * + * @param refType the type of reg to search for e.g. tags or commits + * @return paged iterable of all refs of the specified type + * @throws IOException on failure communicating with GitHub, potentially due to an invalid ref type being requested + */ + public PagedIterable listRefs(String refType) throws IOException { + final String url = String.format("/repos/%s/%s/git/refs/%s", getOwnerName(), name, refType); + return new PagedIterable() { + public PagedIterator _iterator(int pageSize) { + return new PagedIterator(root.retrieve().asIterator(url, GHRef[].class, pageSize)) { + protected void wrapUp(GHRef[] page) { + // no-op + } + }; + } + }; + } + /** * Retrive a ref of the given type for the current GitHub repository. * @@ -810,6 +857,18 @@ public GHRef getRef(String refName) throws IOException { refName = refName.replaceAll("#", "%23"); return root.retrieve().to(String.format("/repos/%s/%s/git/refs/%s", getOwnerName(), name, refName), GHRef.class).wrap(root); } + + /** + * Returns the annotated tag object. Only valid if the {@link GHRef#getObject()} has a + * {@link GHRef.GHObject#getType()} of {@code tag}. + * + * @param sha the sha of the tag object + * @return the annotated tag object + */ + public GHTagObject getTagObject(String sha) throws IOException { + return root.retrieve().to(getApiTailUrl("git/tags/" + sha), GHTagObject.class).wrap(this); + } + /** * Retrive a tree of the given type for the current GitHub repository. * @@ -1146,7 +1205,7 @@ public GHHook createWebHook(URL url) throws IOException { * @deprecated * Use {@link #getHooks()} and {@link #createHook(String, Map, Collection, boolean)} */ - @SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS", + @SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS", justification = "It causes a performance degradation, but we have already exposed it to the API") public Set getPostCommitHooks() { return postCommitHooks; @@ -1155,7 +1214,7 @@ public Set getPostCommitHooks() { /** * Live set view of the post-commit hook. */ - @SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS", + @SuppressFBWarnings(value = "DMI_COLLECTION_OF_URLS", justification = "It causes a performance degradation, but we have already exposed it to the API") @SkipFromToString private final Set postCommitHooks = new AbstractSet() { @@ -1223,7 +1282,7 @@ public boolean remove(Object url) { */ public Map getBranches() throws IOException { Map r = new TreeMap(); - for (GHBranch p : root.retrieve().to(getApiTailUrl("branches"), GHBranch[].class)) { + for (GHBranch p : root.retrieve().withPreview(LOKI).to(getApiTailUrl("branches"), GHBranch[].class)) { p.wrap(this); r.put(p.getName(),p); } @@ -1231,7 +1290,7 @@ public Map getBranches() throws IOException { } public GHBranch getBranch(String name) throws IOException { - return root.retrieve().to(getApiTailUrl("branches/"+name),GHBranch.class).wrap(this); + return root.retrieve().withPreview(LOKI).to(getApiTailUrl("branches/"+name),GHBranch.class).wrap(this); } /** @@ -1453,7 +1512,7 @@ public int hashCode() { public boolean equals(Object obj) { // We ignore contributions in the calculation return super.equals(obj); - } + } } /** diff --git a/src/main/java/org/kohsuke/github/GHTagObject.java b/src/main/java/org/kohsuke/github/GHTagObject.java new file mode 100644 index 0000000000..a95571c479 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHTagObject.java @@ -0,0 +1,60 @@ +package org.kohsuke.github; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +/** + * Represents an annotated tag in a {@link GHRepository} + * + * @see GHRepository#getAnnotatedTag() + */ +@SuppressFBWarnings(value = {"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", + "NP_UNWRITTEN_FIELD"}, justification = "JSON API") +public class GHTagObject { + private GHRepository owner; + private GitHub root; + + private String tag; + private String sha; + private String url; + private String message; + private GitUser tagger; + private GHRef.GHObject object; + + /*package*/ GHTagObject wrap(GHRepository owner) { + this.owner = owner; + this.root = owner.root; + return this; + } + + public GHRepository getOwner() { + return owner; + } + + public GitHub getRoot() { + return root; + } + + public String getTag() { + return tag; + } + + public String getSha() { + return sha; + } + + public String getUrl() { + return url; + } + + public String getMessage() { + return message; + } + + public GitUser getTagger() { + return tagger; + } + + public GHRef.GHObject getObject() { + return object; + } +} diff --git a/src/main/java/org/kohsuke/github/GitHub.java b/src/main/java/org/kohsuke/github/GitHub.java index 896fb36c63..49c860e4dc 100644 --- a/src/main/java/org/kohsuke/github/GitHub.java +++ b/src/main/java/org/kohsuke/github/GitHub.java @@ -719,6 +719,14 @@ private boolean isPrivateModeEnabled() { } } + /** + * Search commits. + */ + @Preview @Deprecated + public GHCommitSearchBuilder searchCommits() { + return new GHCommitSearchBuilder(this); + } + /** * Search issues. */ diff --git a/src/main/java/org/kohsuke/github/Previews.java b/src/main/java/org/kohsuke/github/Previews.java index 238b062b8b..2bc6dccbc8 100644 --- a/src/main/java/org/kohsuke/github/Previews.java +++ b/src/main/java/org/kohsuke/github/Previews.java @@ -7,5 +7,6 @@ static final String LOKI = "application/vnd.github.loki-preview+json"; static final String DRAX = "application/vnd.github.drax-preview+json"; static final String SQUIRREL_GIRL = "application/vnd.github.squirrel-girl-preview"; - static final String KORRA = "application/vnd.github.korra-preview"; + static final String CLOAK = "application/vnd.github.cloak-preview"; + static final String BLACK_CAT = "application/vnd.github.black-cat-preview+json"; } diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index d7d6239190..576d313f5d 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -170,6 +170,11 @@ public Requester with(@WillClose/*later*/ InputStream body) { return this; } + public Requester withNullable(String key, Object value) { + args.add(new Entry(key, value)); + return this; + } + public Requester _with(String key, Object value) { if (value!=null) { args.add(new Entry(key,value)); @@ -314,7 +319,7 @@ public InputStream asStream(String tailApiUrl) throws IOException { setupConnection(root.getApiURL(tailApiUrl)); buildRequest(); - + try { return wrapStream(uc.getInputStream()); } catch (IOException e) { diff --git a/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java b/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java index f32087c0d1..a7afb38fd7 100644 --- a/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java +++ b/src/test/java/org/kohsuke/github/AbstractGitHubApiTestBase.java @@ -1,5 +1,8 @@ package org.kohsuke.github; +import java.io.FileInputStream; +import java.util.Properties; +import org.apache.commons.io.IOUtils; import org.junit.Assert; import org.junit.Assume; import org.junit.Before; @@ -19,9 +22,17 @@ public abstract class AbstractGitHubApiTestBase extends Assert { public void setUp() throws Exception { File f = new File(System.getProperty("user.home"), ".github.kohsuke2"); if (f.exists()) { + Properties props = new Properties(); + FileInputStream in = null; + try { + in = new FileInputStream(f); + props.load(in); + } finally { + IOUtils.closeQuietly(in); + } // use the non-standard credential preferentially, so that developers of this library do not have // to clutter their event stream. - gitHub = GitHubBuilder.fromPropertyFile(f.getPath()).withRateLimitHandler(RateLimitHandler.FAIL).build(); + gitHub = GitHubBuilder.fromProperties(props).withRateLimitHandler(RateLimitHandler.FAIL).build(); } else { gitHub = GitHubBuilder.fromCredentials().withRateLimitHandler(RateLimitHandler.FAIL).build(); } diff --git a/src/test/java/org/kohsuke/github/AppTest.java b/src/test/java/org/kohsuke/github/AppTest.java index a20b2eccf3..248bd0ef11 100755 --- a/src/test/java/org/kohsuke/github/AppTest.java +++ b/src/test/java/org/kohsuke/github/AppTest.java @@ -685,6 +685,15 @@ public void testMemberPagenation() throws IOException { assertFalse(all.isEmpty()); } + @Test + public void testCommitSearch() throws IOException { + PagedSearchIterable r = gitHub.searchCommits().author("kohsuke").list(); + assertTrue(r.getTotalCount() > 0); + + GHCommit firstCommit = r.iterator().next(); + assertTrue(firstCommit.getFiles().size() > 0); + } + @Test public void testIssueSearch() throws IOException { PagedSearchIterable r = gitHub.searchIssues().mentions("kohsuke").isOpen().list(); diff --git a/src/test/java/org/kohsuke/github/GHBranchProtectionTest.java b/src/test/java/org/kohsuke/github/GHBranchProtectionTest.java new file mode 100644 index 0000000000..b7ae713d87 --- /dev/null +++ b/src/test/java/org/kohsuke/github/GHBranchProtectionTest.java @@ -0,0 +1,82 @@ +package org.kohsuke.github; + +import org.junit.Before; +import org.junit.Test; +import org.kohsuke.github.GHBranchProtection.EnforceAdmins; +import org.kohsuke.github.GHBranchProtection.RequiredReviews; +import org.kohsuke.github.GHBranchProtection.RequiredStatusChecks; + +import java.io.FileNotFoundException; + +public class GHBranchProtectionTest extends AbstractGitHubApiTestBase { + private static final String BRANCH = "bp-test"; + private static final String BRANCH_REF = "heads/" + BRANCH; + + private GHBranch branch; + + private GHRepository repo; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + repo = gitHub.getRepository("github-api-test-org/GHContentIntegrationTest").fork(); + + try { + repo.getRef(BRANCH_REF); + } catch (FileNotFoundException e) { + repo.createRef("refs/" + BRANCH_REF, repo.getBranch("master").getSHA1()); + } + + branch = repo.getBranch(BRANCH); + + if (branch.isProtected()) { + branch.disableProtection(); + } + + branch = repo.getBranch(BRANCH); + assertFalse(branch.isProtected()); + } + + @Test + public void testEnableBranchProtections() throws Exception { + // team/user restrictions require an organization repo to test against + GHBranchProtection protection = branch.enableProtection() + .addRequiredChecks("test-status-check") + .requireBranchIsUpToDate() + .requireCodeOwnReviews() + .dismissStaleReviews() + .includeAdmins() + .enable(); + + RequiredStatusChecks statusChecks = protection.getRequiredStatusChecks(); + assertNotNull(statusChecks); + assertTrue(statusChecks.isRequiresBranchUpToDate()); + assertTrue(statusChecks.getContexts().contains("test-status-check")); + + RequiredReviews requiredReviews = protection.getRequiredReviews(); + assertNotNull(requiredReviews); + assertTrue(requiredReviews.isDismissStaleReviews()); + assertTrue(requiredReviews.isRequireCodeOwnerReviews()); + + EnforceAdmins enforceAdmins = protection.getEnforceAdmins(); + assertNotNull(enforceAdmins); + assertTrue(enforceAdmins.isEnabled()); + } + + @Test + public void testEnableProtectionOnly() throws Exception { + branch.enableProtection().enable(); + assertTrue(repo.getBranch(BRANCH).isProtected()); + } + + @Test + public void testEnableRequireReviewsOnly() throws Exception { + GHBranchProtection protection = branch.enableProtection() + .requireReviews() + .enable(); + + assertNotNull(protection.getRequiredReviews()); + } +} diff --git a/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java b/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java index c642d662e7..8e3873708b 100644 --- a/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java +++ b/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java @@ -20,13 +20,6 @@ public void setUp() throws Exception { repo = gitHub.getRepository("github-api-test-org/GHContentIntegrationTest").fork(); } - @Test - public void testBranchProtection() throws Exception { - GHBranch b = repo.getBranch("master"); - b.enableProtection(EnforcementLevel.NON_ADMINS, "foo/bar"); - b.disableProtection(); - } - @Test public void testGetFileContent() throws Exception { GHContent content = repo.getFileContent("ghcontent-ro/a-file-with-content"); diff --git a/src/test/java/org/kohsuke/github/PullRequestTest.java b/src/test/java/org/kohsuke/github/PullRequestTest.java index c7ce77d289..ce09b2ab13 100644 --- a/src/test/java/org/kohsuke/github/PullRequestTest.java +++ b/src/test/java/org/kohsuke/github/PullRequestTest.java @@ -7,6 +7,9 @@ import java.util.Collection; import java.util.List; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; + /** * @author Kohsuke Kawaguchi */ @@ -26,6 +29,33 @@ public void createPullRequestComment() throws Exception { p.comment("Some comment"); } + @Test + public void testPullRequestReviews() throws Exception { + String name = rnd.next(); + GHPullRequest p = getRepository().createPullRequest(name, "stable", "master", "## test"); + GHPullRequestReview draftReview = p.createReview("Some draft review", null, + GHPullRequestReviewComment.draft("Some niggle", "changelog.html", 1) + ); + assertThat(draftReview.getState(), is(GHPullRequestReviewState.PENDING)); + assertThat(draftReview.getBody(), is("Some draft review")); + assertThat(draftReview.getCommitId(), notNullValue()); + List reviews = p.listReviews().asList(); + assertThat(reviews.size(), is(1)); + GHPullRequestReview review = reviews.get(0); + assertThat(review.getState(), is(GHPullRequestReviewState.PENDING)); + assertThat(review.getBody(), is("Some draft review")); + assertThat(review.getCommitId(), notNullValue()); + review.submit("Some review comment", GHPullRequestReviewState.COMMENTED); + List comments = review.listReviewComments().asList(); + assertEquals(1, comments.size()); + GHPullRequestReviewComment comment = comments.get(0); + assertEquals("Some niggle", comment.getBody()); + review = p.createReview("Some new review", null, + GHPullRequestReviewComment.draft("Some niggle", "changelog.html", 1) + ); + review.delete(); + } + @Test public void testPullRequestReviewComments() throws Exception { String name = rnd.next(); diff --git a/src/test/java/org/kohsuke/github/RepositoryTest.java b/src/test/java/org/kohsuke/github/RepositoryTest.java index 9955c34956..728f94a834 100644 --- a/src/test/java/org/kohsuke/github/RepositoryTest.java +++ b/src/test/java/org/kohsuke/github/RepositoryTest.java @@ -67,6 +67,32 @@ public void getPermission() throws Exception { } } } + + + + @Test + public void LatestRepositoryExist() { + try { + // add the repository that have latest release + GHRelease release = gitHub.getRepository("kamontat/CheckIDNumber").getLatestRelease(); + assertEquals("v3.0", release.getTagName()); + } catch (IOException e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void LatestRepositoryNotExist() { + try { + // add the repository that `NOT` have latest release + GHRelease release = gitHub.getRepository("kamontat/Java8Example").getLatestRelease(); + assertNull(release); + } catch (IOException e) { + e.printStackTrace(); + fail(); + } + } private GHRepository getRepository() throws IOException { return gitHub.getOrganization("github-api-test-org").getRepository("jenkins");