diff --git a/.gitignore b/.gitignore index a4bc85968..3c2c3c642 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ target *.iml *.ipr *.iws -/.project -/.classpath -/.settings +.classpath +.project +.settings/ +.DS_Store 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 4ae03b162..000000000 --- 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/GHBranch.java b/src/main/java/org/kohsuke/github/GHBranch.java index f54bea9f4..bdfe11899 100644 --- a/src/main/java/org/kohsuke/github/GHBranch.java +++ b/src/main/java/org/kohsuke/github/GHBranch.java @@ -4,10 +4,6 @@ import static org.kohsuke.github.Previews.LOKI; 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; @@ -15,10 +11,10 @@ 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 +29,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 +65,10 @@ public class GHBranch { 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 +82,7 @@ public class GHBranch { */ @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 +91,14 @@ public class GHBranch { * @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)); - } - - private void setProtection(BranchProtection bp) throws IOException { - new Requester(root).method("PATCH").withPreview(LOKI)._with("protection",bp).to(getApiRoute()); + public GHBranchProtectionBuilder enableProtection() { + return new GHBranchProtectionBuilder(this); } 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 000000000..321636611 --- /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 000000000..fc521fff3 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GHBranchProtectionBuilder.java @@ -0,0 +1,186 @@ +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; + +@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() { + enforceAdmins = true; + return this; + } + + public GHBranchProtectionBuilder requireBranchIsUpToDate() { + getStatusChecks().strict = true; + return this; + } + + public GHBranchProtectionBuilder requireCodeOwnReviews() { + getPrReviews().put("require_code_owner_reviews", true); + 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/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 0f9e252ab..04017787b 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -60,7 +60,7 @@ import static org.kohsuke.github.Previews.*; * @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; @@ -442,8 +442,8 @@ public class GHRepository extends GHObject { public int getSize() { return size; } - - + + /** * Gets the collaborators on this repository. * This set always appear to include the owner. @@ -1152,7 +1152,7 @@ public class GHRepository extends GHObject { * @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; @@ -1161,7 +1161,7 @@ public class GHRepository extends GHObject { /** * 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() { @@ -1229,7 +1229,7 @@ public class GHRepository extends GHObject { */ 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); } @@ -1237,7 +1237,7 @@ public class GHRepository extends GHObject { } 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); } /** @@ -1459,7 +1459,7 @@ public class GHRepository extends GHObject { public boolean equals(Object obj) { // We ignore contributions in the calculation return super.equals(obj); - } + } } /** diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index d7d623919..576d313f5 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -170,6 +170,11 @@ class Requester { 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 @@ class Requester { setupConnection(root.getApiURL(tailApiUrl)); buildRequest(); - + try { return wrapStream(uc.getInputStream()); } catch (IOException e) { 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 000000000..a6a8a831c --- /dev/null +++ b/src/test/java/org/kohsuke/github/GHBranchProtectionTest.java @@ -0,0 +1,78 @@ +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; + +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(); + + if (repo.getRef(BRANCH_REF) == null) { + 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 c642d662e..8e3873708 100644 --- a/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java +++ b/src/test/java/org/kohsuke/github/GHContentIntegrationTest.java @@ -20,13 +20,6 @@ public class GHContentIntegrationTest extends AbstractGitHubApiTestBase { 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");