diff --git a/pom.xml b/pom.xml index 385e60d87..66b66cb24 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ 2.528 2.539 + natural-comparator false true @@ -77,6 +78,11 @@ io.jenkins.plugins okhttp-api + + net.grey-panther + natural-comparator + 1.1 + org.jenkins-ci.plugins credentials diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java index f06c109b2..cf89444ba 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration.java @@ -55,6 +55,10 @@ public static GitHubConfiguration get() { private ApiRateLimitChecker apiRateLimitChecker; + private boolean tagDescendingOrder; + + private int maxTagCount; + public GitHubConfiguration() { load(); } @@ -83,6 +87,44 @@ public synchronized void setApiRateLimitChecker(@CheckForNull ApiRateLimitChecke save(); } + /** + * Returns whether tags should be scanned in descending order by default. + * + * @return {@code true} if tags should be scanned in descending order by default. + */ + public synchronized boolean isTagDescendingOrder() { + return tagDescendingOrder; + } + + /** + * Sets whether tags should be scanned in descending order by default. + * + * @param tagDescendingOrder {@code true} to scan tags in descending order by default. + */ + public synchronized void setTagDescendingOrder(boolean tagDescendingOrder) { + this.tagDescendingOrder = tagDescendingOrder; + save(); + } + + /** + * Returns the default maximum number of tags to process (0 = unlimited). + * + * @return the default maximum number of tags to process. + */ + public synchronized int getMaxTagCount() { + return maxTagCount; + } + + /** + * Sets the default maximum number of tags to process. + * + * @param maxTagCount maximum number of tags (0 = unlimited). + */ + public synchronized void setMaxTagCount(int maxTagCount) { + this.maxTagCount = maxTagCount; + save(); + } + /** * Fix an apiUri. * diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index ac1c480fc..1c18e8549 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -117,6 +117,7 @@ import jenkins.scm.impl.trait.Selection; import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; import jenkins.util.SystemProperties; +import net.greypanther.natsort.SimpleNaturalComparator; import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.lib.Constants; import org.jenkinsci.Symbol; @@ -1185,6 +1186,11 @@ public SCMSourceCriteria.Probe create( listener.getLogger().format("%n %d tags were processed (query completed)%n", count); break; } + int maxTagCount = request.getMaxTagCount(); + if (maxTagCount > 0 && count >= maxTagCount) { + listener.getLogger().format("%n %d tags were processed (limit reached)%n", count); + break; + } } listener.getLogger().format("%n %d tags were processed%n", count); } @@ -2665,10 +2671,12 @@ public int compare(GHBranch o1, GHBranch o2) { static class LazyTags extends LazyIterable { private final GitHubSCMSourceRequest request; private final GHRepository repo; + private final boolean descendingOrder; public LazyTags(GitHubSCMSourceRequest request, GHRepository repo) { this.request = request; this.repo = repo; + this.descendingOrder = request.isTagDescendingOrder(); } @Override @@ -2701,77 +2709,102 @@ protected Iterable create() { // // Instead we just return a wrapped iterator that does the right thing. final Iterable iterable = repo.listRefs("tags"); + + if (descendingOrder) { + // Collect all tags and reverse for descending order. + // This loses lazy-loading but the GitHub REST API does not support + // reverse ordering on refs, so eager collection is unavoidable. + List allTags = collectTags(iterable); + allTags.sort(Comparator.comparing( + (GHRef ref) -> ref.getRef(), + SimpleNaturalComparator.getInstance().reversed())); + return allTags; + } + return new Iterable() { @Override public Iterator iterator() { - final Iterator iterator; - try { - iterator = iterable.iterator(); - } catch (Error e) { - if (e.getCause() instanceof GHFileNotFoundException) { - return Collections.emptyIterator(); - } - throw e; - } - return new Iterator() { - boolean hadAtLeastOne; - boolean hasNone; - - @Override - public boolean hasNext() { - try { - boolean hasNext = iterator.hasNext(); - hadAtLeastOne = hadAtLeastOne || hasNext; - return hasNext; - } catch (Error e) { - // pre https://github.com/kohsuke/github-api/commit - // /a17ce04552ddd3f6bd8210c740184e6c7ad13ae4 - // we at least got the cause, even if wrapped in an Error - if (e.getCause() instanceof GHFileNotFoundException) { - return false; - } - throw e; - } catch (GHException e) { - // JENKINS-52397 I have no clue why https://github.com/kohsuke/github-api/commit - // /a17ce04552ddd3f6bd8210c740184e6c7ad13ae4 does what it does, but it makes - // it rather difficult to distinguish between a network outage and the file - // not found. - if (hadAtLeastOne) { - throw e; - } - try { - hasNone = hasNone || repo.getRefs("tags").length == 0; - if (hasNone) return false; - throw e; - } catch (FileNotFoundException e1) { - hasNone = true; - return false; - } catch (IOException e1) { - e.addSuppressed(e1); - throw e; - } - } - } - - @Override - public GHRef next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - return iterator.next(); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("remove"); - } - }; + return safeTagIterator(iterable); } }; } catch (IOException e) { throw new GitHubSCMSource.WrappedException(e); } } + + private List collectTags(Iterable iterable) { + List result = new ArrayList<>(); + Iterator it = safeTagIterator(iterable); + while (it.hasNext()) { + result.add(it.next()); + } + return result; + } + + private Iterator safeTagIterator(Iterable iterable) { + final Iterator iterator; + try { + iterator = iterable.iterator(); + } catch (Error e) { + if (e.getCause() instanceof GHFileNotFoundException) { + return Collections.emptyIterator(); + } + throw e; + } + return new Iterator() { + boolean hadAtLeastOne; + boolean hasNone; + + @Override + public boolean hasNext() { + try { + boolean hasNext = iterator.hasNext(); + hadAtLeastOne = hadAtLeastOne || hasNext; + return hasNext; + } catch (Error e) { + // pre https://github.com/kohsuke/github-api/commit + // /a17ce04552ddd3f6bd8210c740184e6c7ad13ae4 + // we at least got the cause, even if wrapped in an Error + if (e.getCause() instanceof GHFileNotFoundException) { + return false; + } + throw e; + } catch (GHException e) { + // JENKINS-52397 I have no clue why https://github.com/kohsuke/github-api/commit + // /a17ce04552ddd3f6bd8210c740184e6c7ad13ae4 does what it does, but it makes + // it rather difficult to distinguish between a network outage and the file + // not found. + if (hadAtLeastOne) { + throw e; + } + try { + hasNone = hasNone || repo.getRefs("tags").length == 0; + if (hasNone) return false; + throw e; + } catch (FileNotFoundException e1) { + hasNone = true; + return false; + } catch (IOException e1) { + e.addSuppressed(e1); + throw e; + } + } + } + + @Override + public GHRef next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return iterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + }; + } } private static class CriteriaWitness implements SCMSourceRequest.Witness { diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java index 6e764c68a..da939fe10 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceContext.java @@ -63,6 +63,10 @@ public class GitHubSCMSourceContext extends SCMSourceContext forkPRStrategies = EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); + /** {@code true} if tags should be scanned in descending (reverse alphabetical) order. */ + private boolean tagDescendingOrder; + /** Maximum number of tags to process (0 = unlimited). */ + private int maxTagCount; /** {@code true} if notifications should be disabled in this context. */ private boolean notificationsDisabled; /** @@ -102,6 +106,24 @@ public final boolean wantTags() { return wantTags; } + /** + * Returns {@code true} if tags should be scanned in descending order. + * + * @return {@code true} if tags should be scanned in descending order. + */ + public final boolean isTagDescendingOrder() { + return tagDescendingOrder; + } + + /** + * Returns the maximum number of tags to process (0 = unlimited). + * + * @return the maximum number of tags to process. + */ + public final int getMaxTagCount() { + return maxTagCount; + } + /** * Returns {@code true} if the {@link GitHubSCMSourceRequest} will need information about pull * requests. @@ -203,6 +225,30 @@ public GitHubSCMSourceContext wantTags(boolean include) { return this; } + /** + * Sets whether tags should be scanned in descending (reverse alphabetical) order. + * + * @param descending {@code true} to scan tags in descending order. + * @return {@code this} for method chaining. + */ + @NonNull + public GitHubSCMSourceContext withTagDescendingOrder(boolean descending) { + tagDescendingOrder = descending; + return this; + } + + /** + * Sets the maximum number of tags to process. + * + * @param maxTagCount maximum number of tags (0 = unlimited). + * @return {@code this} for method chaining. + */ + @NonNull + public GitHubSCMSourceContext withMaxTagCount(int maxTagCount) { + this.maxTagCount = maxTagCount; + return this; + } + /** * Adds a requirement for origin pull request details to any {@link GitHubSCMSourceRequest} for * this context. diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java index ea2329eec..af5e9d038 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSourceRequest.java @@ -59,6 +59,10 @@ public class GitHubSCMSourceRequest extends SCMSourceRequest { private final boolean fetchBranches; /** {@code true} if tag details need to be fetched. */ private final boolean fetchTags; + /** {@code true} if tags should be scanned in descending order. */ + private final boolean tagDescendingOrder; + /** Maximum number of tags to process (0 = unlimited). */ + private final int maxTagCount; /** {@code true} if origin pull requests need to be fetched. */ private final boolean fetchOriginPRs; /** {@code true} if fork pull requests need to be fetched. */ @@ -124,6 +128,8 @@ public class GitHubSCMSourceRequest extends SCMSourceRequest { super(source, context, listener); fetchBranches = context.wantBranches(); fetchTags = context.wantTags(); + tagDescendingOrder = context.isTagDescendingOrder(); + maxTagCount = context.getMaxTagCount(); fetchOriginPRs = context.wantOriginPRs(); fetchForkPRs = context.wantForkPRs(); originPRStrategies = fetchOriginPRs && !context.originPRStrategies().isEmpty() @@ -177,6 +183,24 @@ public final boolean isFetchTags() { return fetchTags; } + /** + * Returns {@code true} if tags should be scanned in descending order. + * + * @return {@code true} if tags should be scanned in descending order. + */ + public final boolean isTagDescendingOrder() { + return tagDescendingOrder; + } + + /** + * Returns the maximum number of tags to process (0 = unlimited). + * + * @return the maximum number of tags to process. + */ + public final int getMaxTagCount() { + return maxTagCount; + } + /** * Returns {@code true} if pull request details need to be fetched. * diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java index 29b05a5b3..52251b623 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait.java @@ -23,6 +23,7 @@ */ package org.jenkinsci.plugins.github_branch_source; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import jenkins.plugins.git.GitTagSCMRevision; @@ -39,6 +40,7 @@ import jenkins.scm.impl.trait.Discovery; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; /** * A {@link Discovery} trait for GitHub that will discover tags on the repository. @@ -46,15 +48,65 @@ * @since 2.3.0 */ public class TagDiscoveryTrait extends SCMSourceTrait { + /** + * When {@code null}, the global default from {@link GitHubConfiguration#isTagDescendingOrder()} + * is used. When explicitly set, overrides the global default. + */ + @CheckForNull + private Boolean descendingOrder; + + /** + * When {@code null}, the global default from {@link GitHubConfiguration#getMaxTagCount()} + * is used. When explicitly set, overrides the global default. + */ + @CheckForNull + private Integer maxTagCount; + /** Constructor for stapler. */ @DataBoundConstructor public TagDiscoveryTrait() {} + /** + * Returns the effective descending order setting, resolving the global default + * when no per-job override has been set. + */ + public boolean isDescendingOrder() { + if (descendingOrder != null) { + return descendingOrder; + } + GitHubConfiguration cfg = GitHubConfiguration.get(); + return cfg != null && cfg.isTagDescendingOrder(); + } + + @DataBoundSetter + public void setDescendingOrder(boolean descendingOrder) { + this.descendingOrder = descendingOrder; + } + + /** + * Returns the effective max tag count, resolving the global default + * when no per-job override has been set. + */ + public int getMaxTagCount() { + if (maxTagCount != null) { + return maxTagCount; + } + GitHubConfiguration cfg = GitHubConfiguration.get(); + return cfg != null ? cfg.getMaxTagCount() : 0; + } + + @DataBoundSetter + public void setMaxTagCount(int maxTagCount) { + this.maxTagCount = maxTagCount; + } + /** {@inheritDoc} */ @Override protected void decorateContext(SCMSourceContext context) { GitHubSCMSourceContext ctx = (GitHubSCMSourceContext) context; ctx.wantTags(true); + ctx.withTagDescendingOrder(isDescendingOrder()); + ctx.withMaxTagCount(getMaxTagCount()); ctx.withAuthority(new TagSCMHeadAuthority()); } diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly index ad20040b0..556ee6518 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/config.jelly @@ -5,6 +5,13 @@ + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-maxTagCount.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-maxTagCount.html new file mode 100644 index 000000000..a615ecbf7 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-maxTagCount.html @@ -0,0 +1,6 @@ +
+ Limits the number of tags to process during tag discovery. Set to 0 + (the default) for no limit. This setting is only effective when + descending order is enabled. Individual jobs can override this setting + in their Tag Discovery trait configuration. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-tagDescendingOrder.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-tagDescendingOrder.html new file mode 100644 index 000000000..28ed2b35b --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubConfiguration/help-tagDescendingOrder.html @@ -0,0 +1,6 @@ +
+ When checked, tag discovery will scan tags in descending (reverse alphabetical) + order by default across all jobs using GitHub tag discovery. This is useful when you + want the most recent semantic version tags to be discovered first. Individual + jobs can override this setting in their Tag Discovery trait configuration. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly index d89935c79..40eb02f23 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/config.jelly @@ -2,4 +2,9 @@ + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html new file mode 100644 index 000000000..6090c3080 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-descendingOrder.html @@ -0,0 +1,7 @@ +
+ When checked, tags will be scanned in descending (reverse alphabetical) order + instead of the default ascending order. This is useful when you want the + most recent semantic version tags to be discovered first. This setting + overrides the global default configured in Manage Jenkins » System + » Tag Discovery. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-maxTagCount.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-maxTagCount.html new file mode 100644 index 000000000..7c432caae --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTrait/help-maxTagCount.html @@ -0,0 +1,6 @@ +
+ Limits the number of tags to process during tag discovery. Set to 0 + (the default) for no limit. This setting is only effective when + descending order is enabled. This setting overrides the global default + configured in Manage Jenkins » System » Tag Discovery. +
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java index 806812fe8..eecb92c2d 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubSCMSourceTagsTest.java @@ -8,10 +8,12 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; import jenkins.scm.api.SCMHeadObserver; import org.junit.Test; @@ -150,6 +152,86 @@ public void testExistingMultipleTags() throws IOException { } } + @Test + public void testNaturalSortDescendingOrder() throws IOException { + // Scenario: Tags with version-like names where natural and alphabetical sort differ. + // Fixture returns alphabetical order: v0.1.11, v0.1.2, v0.10.0, v0.2.0 + // Natural descending should give: v0.10.0, v0.2.0, v0.1.11, v0.1.2 + // (Alphabetical descending would give: v0.2.0, v0.10.0, v0.1.2, v0.1.11 -- wrong) + SCMHeadObserver mockSCMHeadObserver = Mockito.mock(SCMHeadObserver.class); + Mockito.when(mockSCMHeadObserver.getIncludes()).thenReturn(null); + + GHRepository versionedRepo = github.getRepository("cloudbeers/yolo-versioned"); + + GitHubSCMSourceContext context = new GitHubSCMSourceContext(null, mockSCMHeadObserver); + context.wantTags(true); + context.withTagDescendingOrder(true); + GitHubSCMSourceRequest request = + context.newRequest(new GitHubSCMSource("cloudbeers", "yolo-versioned", null, false), null); + Iterator tags = new GitHubSCMSource.LazyTags(request, versionedRepo).iterator(); + + List tagRefs = new ArrayList<>(); + while (tags.hasNext()) { + tagRefs.add(tags.next().getRef()); + } + assertEquals(4, tagRefs.size()); + assertEquals("refs/tags/v0.10.0", tagRefs.get(0)); + assertEquals("refs/tags/v0.2.0", tagRefs.get(1)); + assertEquals("refs/tags/v0.1.11", tagRefs.get(2)); + assertEquals("refs/tags/v0.1.2", tagRefs.get(3)); + } + + @Test + public void testExistingMultipleTagsDescendingOrder() throws IOException { + // Scenario: Requesting multiple tags in descending order + SCMHeadObserver mockSCMHeadObserver = Mockito.mock(SCMHeadObserver.class); + + Mockito.when(mockSCMHeadObserver.getIncludes()) + .thenReturn(new HashSet<>(Arrays.asList( + new GitHubTagSCMHead("existent-multiple-tags1", System.currentTimeMillis()), + new GitHubTagSCMHead("existent-multiple-tags2", System.currentTimeMillis())))); + GitHubSCMSourceContext context = new GitHubSCMSourceContext(null, mockSCMHeadObserver); + context.wantTags(true); + context.withTagDescendingOrder(true); + GitHubSCMSourceRequest request = + context.newRequest(new GitHubSCMSource("cloudbeers", "yolo", null, false), null); + assertTrue(request.isTagDescendingOrder()); + Iterator tags = new GitHubSCMSource.LazyTags(request, repo).iterator(); + + // Expected: Tags should be returned in reverse (descending) order + List tagRefs = new ArrayList<>(); + while (tags.hasNext()) { + tagRefs.add(tags.next().getRef()); + } + assertEquals(2, tagRefs.size()); + assertEquals("refs/tags/existent-multiple-tags2", tagRefs.get(0)); + assertEquals("refs/tags/existent-multiple-tags1", tagRefs.get(1)); + } + + @Test + public void testExistingMultipleTagsAscendingOrderByDefault() throws IOException { + // Scenario: Requesting multiple tags without descending flag (default ascending) + SCMHeadObserver mockSCMHeadObserver = Mockito.mock(SCMHeadObserver.class); + + Mockito.when(mockSCMHeadObserver.getIncludes()) + .thenReturn(new HashSet<>(Arrays.asList( + new GitHubTagSCMHead("existent-multiple-tags1", System.currentTimeMillis()), + new GitHubTagSCMHead("existent-multiple-tags2", System.currentTimeMillis())))); + GitHubSCMSourceContext context = new GitHubSCMSourceContext(null, mockSCMHeadObserver); + context.wantTags(true); + GitHubSCMSourceRequest request = + context.newRequest(new GitHubSCMSource("cloudbeers", "yolo", null, false), null); + assertFalse(request.isTagDescendingOrder()); + Iterator tags = new GitHubSCMSource.LazyTags(request, repo).iterator(); + + // Expected: Tags should be returned in ascending (default) order + assertTrue(tags.hasNext()); + assertEquals("refs/tags/existent-multiple-tags1", tags.next().getRef()); + assertTrue(tags.hasNext()); + assertEquals("refs/tags/existent-multiple-tags2", tags.next().getRef()); + assertFalse(tags.hasNext()); + } + @Test public void testExistingMultipleTagsGHFileNotFoundExceptionIterable() throws IOException { // Scenario: Requesting multiple tags but a FileNotFound is thrown diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java index e4e52e83c..bc9ac4714 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/TagDiscoveryTraitTest.java @@ -24,6 +24,7 @@ public class TagDiscoveryTraitTest { @Test public void decorateContext() throws Exception { + GitHubConfiguration.get().setTagDescendingOrder(false); GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); assertThat(probe.wantBranches(), is(false)); assertThat(probe.wantPRs(), is(false)); @@ -33,9 +34,90 @@ public void decorateContext() throws Exception { assertThat(probe.wantBranches(), is(false)); assertThat(probe.wantPRs(), is(false)); assertThat(probe.wantTags(), is(true)); + assertThat(probe.isTagDescendingOrder(), is(false)); assertThat(probe.authorities(), contains(instanceOf(TagDiscoveryTrait.TagSCMHeadAuthority.class))); } + @Test + public void decorateContextDescendingOrder() throws Exception { + GitHubConfiguration.get().setTagDescendingOrder(false); + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + assertThat(probe.isTagDescendingOrder(), is(false)); + TagDiscoveryTrait trait = new TagDiscoveryTrait(); + trait.setDescendingOrder(true); + trait.applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.isTagDescendingOrder(), is(true)); + } + + @Test + public void decorateContextUsesGlobalDefault() throws Exception { + GitHubConfiguration.get().setTagDescendingOrder(true); + try { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + new TagDiscoveryTrait().applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.isTagDescendingOrder(), is(true)); + } finally { + GitHubConfiguration.get().setTagDescendingOrder(false); + } + } + + @Test + public void decorateContextPerJobOverridesGlobal() throws Exception { + GitHubConfiguration.get().setTagDescendingOrder(true); + try { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + TagDiscoveryTrait trait = new TagDiscoveryTrait(); + trait.setDescendingOrder(false); + trait.applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.isTagDescendingOrder(), is(false)); + } finally { + GitHubConfiguration.get().setTagDescendingOrder(false); + } + } + + @Test + public void decorateContextMaxTagCount() throws Exception { + GitHubConfiguration.get().setMaxTagCount(0); + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + assertThat(probe.getMaxTagCount(), is(0)); + TagDiscoveryTrait trait = new TagDiscoveryTrait(); + trait.setMaxTagCount(10); + trait.applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.getMaxTagCount(), is(10)); + } + + @Test + public void decorateContextMaxTagCountUsesGlobalDefault() throws Exception { + GitHubConfiguration.get().setMaxTagCount(25); + try { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + new TagDiscoveryTrait().applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.getMaxTagCount(), is(25)); + } finally { + GitHubConfiguration.get().setMaxTagCount(0); + } + } + + @Test + public void decorateContextMaxTagCountPerJobOverridesGlobal() throws Exception { + GitHubConfiguration.get().setMaxTagCount(25); + try { + GitHubSCMSourceContext probe = new GitHubSCMSourceContext(null, SCMHeadObserver.collect()); + TagDiscoveryTrait trait = new TagDiscoveryTrait(); + trait.setMaxTagCount(5); + trait.applyToContext(probe); + assertThat(probe.wantTags(), is(true)); + assertThat(probe.getMaxTagCount(), is(5)); + } finally { + GitHubConfiguration.get().setMaxTagCount(0); + } + } + @Test public void includeCategory() throws Exception { assertThat(new TagDiscoveryTrait().includeCategory(ChangeRequestSCMHeadCategory.DEFAULT), is(false)); diff --git a/src/test/resources/api/__files/body-cloudbeers-yolo-versioned.json b/src/test/resources/api/__files/body-cloudbeers-yolo-versioned.json new file mode 100644 index 000000000..14a2a2136 --- /dev/null +++ b/src/test/resources/api/__files/body-cloudbeers-yolo-versioned.json @@ -0,0 +1,109 @@ +{ + "id": 43041241, + "name": "yolo-versioned", + "full_name": "cloudbeers/yolo-versioned", + "owner": { + "login": "cloudbeers", + "id": 4181899, + "avatar_url": "https://avatars.githubusercontent.com/u/4181899?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/cloudbeers", + "html_url": "https://github.com/cloudbeers", + "followers_url": "https://api.github.com/users/cloudbeers/followers", + "following_url": "https://api.github.com/users/cloudbeers/following{/other_user}", + "gists_url": "https://api.github.com/users/cloudbeers/gists{/gist_id}", + "starred_url": "https://api.github.com/users/cloudbeers/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/cloudbeers/subscriptions", + "organizations_url": "https://api.github.com/users/cloudbeers/orgs", + "repos_url": "https://api.github.com/users/cloudbeers/repos", + "events_url": "https://api.github.com/users/cloudbeers/events{/privacy}", + "received_events_url": "https://api.github.com/users/cloudbeers/received_events", + "type": "Organization", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/cloudbeers/yolo-versioned", + "description": "Versioned tags test repo", + "fork": false, + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned", + "forks_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/forks", + "keys_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/teams", + "hooks_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/hooks", + "issue_events_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/issues/events{/number}", + "events_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/events", + "assignees_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/assignees{/user}", + "branches_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/branches{/branch}", + "tags_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/tags", + "blobs_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/statuses/{sha}", + "languages_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/languages", + "stargazers_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/stargazers", + "contributors_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/contributors", + "subscribers_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/subscribers", + "subscription_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/subscription", + "commits_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/contents/{+path}", + "compare_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/merges", + "archive_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/downloads", + "issues_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/issues{/number}", + "pulls_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/pulls{/number}", + "milestones_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/milestones{/number}", + "notifications_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/labels{/name}", + "releases_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/releases{/id}", + "deployments_url": "https://api.github.com/repos/cloudbeers/yolo-versioned/deployments", + "created_at": "2015-09-24T02:58:30Z", + "updated_at": "2016-12-07T23:55:35Z", + "pushed_at": "2016-12-01T16:07:01Z", + "git_url": "git://github.com/cloudbeers/yolo-versioned.git", + "ssh_url": "git@github.com:cloudbeers/yolo-versioned.git", + "clone_url": "https://github.com/cloudbeers/yolo-versioned.git", + "svn_url": "https://github.com/cloudbeers/yolo-versioned", + "homepage": null, + "size": 3, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 0, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "organization": { + "login": "cloudbeers", + "id": 4181899, + "avatar_url": "https://avatars.githubusercontent.com/u/4181899?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/cloudbeers", + "html_url": "https://github.com/cloudbeers", + "followers_url": "https://api.github.com/users/cloudbeers/followers", + "following_url": "https://api.github.com/users/cloudbeers/following{/other_user}", + "gists_url": "https://api.github.com/users/cloudbeers/gists{/gist_id}", + "starred_url": "https://api.github.com/users/cloudbeers/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/cloudbeers/subscriptions", + "organizations_url": "https://api.github.com/users/cloudbeers/orgs", + "repos_url": "https://api.github.com/users/cloudbeers/repos", + "events_url": "https://api.github.com/users/cloudbeers/events{/privacy}", + "received_events_url": "https://api.github.com/users/cloudbeers/received_events", + "type": "Organization", + "site_admin": false + }, + "network_count": 0, + "subscribers_count": 0 +} diff --git a/src/test/resources/api/__files/body-yolo-versioned-tags.json b/src/test/resources/api/__files/body-yolo-versioned-tags.json new file mode 100644 index 000000000..4e29256db --- /dev/null +++ b/src/test/resources/api/__files/body-yolo-versioned-tags.json @@ -0,0 +1,42 @@ +[ + { + "ref": "refs/tags/v0.1.11", + "node_id": "MDM6UmVmNTU1NDMyMjU6dmVyc2lvbmVkLXYwLjEuMTE=", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs/tags/v0.1.11", + "object": { + "sha": "aa11111111111111111111111111111111111111", + "type": "tag", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags/aa11111111111111111111111111111111111111" + } + }, + { + "ref": "refs/tags/v0.1.2", + "node_id": "MDM6UmVmNTU1NDMyMjU6dmVyc2lvbmVkLXYwLjEuMg==", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs/tags/v0.1.2", + "object": { + "sha": "bb22222222222222222222222222222222222222", + "type": "tag", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags/bb22222222222222222222222222222222222222" + } + }, + { + "ref": "refs/tags/v0.2.0", + "node_id": "MDM6UmVmNTU1NDMyMjU6dmVyc2lvbmVkLXYwLjIuMA==", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs/tags/v0.2.0", + "object": { + "sha": "cc33333333333333333333333333333333333333", + "type": "tag", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags/cc33333333333333333333333333333333333333" + } + }, + { + "ref": "refs/tags/v0.10.0", + "node_id": "MDM6UmVmNTU1NDMyMjU6dmVyc2lvbmVkLXYwLjEwLjA=", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/refs/tags/v0.10.0", + "object": { + "sha": "dd44444444444444444444444444444444444444", + "type": "tag", + "url": "https://api.github.com/repos/cloudbeers/yolo-versioned/git/tags/dd44444444444444444444444444444444444444" + } + } +] diff --git a/src/test/resources/api/mappings/mapping-cloudbeers-yolo-versioned.json b/src/test/resources/api/mappings/mapping-cloudbeers-yolo-versioned.json new file mode 100644 index 000000000..4c520ce47 --- /dev/null +++ b/src/test/resources/api/mappings/mapping-cloudbeers-yolo-versioned.json @@ -0,0 +1,33 @@ +{ + "request": { + "url": "/repos/cloudbeers/yolo-versioned", + "method": "GET" + }, + "response": { + "status": 200, + "bodyFileName": "body-cloudbeers-yolo-versioned.json", + "headers": { + "Server": "GitHub.com", + "Date": "Tue, 06 Dec 2016 15:06:25 GMT", + "Content-Type": "application/json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Status": "200 OK", + "X-RateLimit-Limit": "600", + "X-RateLimit-Remaining": "600", + "X-RateLimit-Reset": "1481039662", + "Cache-Control": "public, max-age=60, s-maxage=60", + "Vary": ["Accept", "Accept-Encoding"], + "ETag": "W/\"12cee9e1d9874cbabcbfaf3b112e8dac\"", + "X-GitHub-Media-Type": "github.v3; format=json", + "Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", + "Access-Control-Allow-Origin": "*", + "Content-Security-Policy": "default-src 'none'", + "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "deny", + "X-XSS-Protection": "1; mode=block", + "X-Served-By": "2d7a5e35115884240089368322196939", + "X-GitHub-Request-Id": "B2A7FE77:629C:AF96C44:5846D3F1" + } + } +} diff --git a/src/test/resources/api/mappings/mapping-yolo-versioned-tags.json b/src/test/resources/api/mappings/mapping-yolo-versioned-tags.json new file mode 100644 index 000000000..76c9af5de --- /dev/null +++ b/src/test/resources/api/mappings/mapping-yolo-versioned-tags.json @@ -0,0 +1,33 @@ +{ + "request": { + "url": "/repos/cloudbeers/yolo-versioned/git/refs/tags", + "method": "GET" + }, + "response": { + "status": 200, + "bodyFileName": "body-yolo-versioned-tags.json", + "headers": { + "Server": "GitHub.com", + "Date": "Mon, 29 Jul 2019 23:22:30 GMT", + "Content-Type": "application/json; charset=utf-8", + "Transfer-Encoding": "chunked", + "Status": "200 OK", + "X-RateLimit-Limit": "600", + "X-RateLimit-Remaining": "595", + "X-RateLimit-Reset": "1481048932", + "Cache-Control": "public, max-age=60, s-maxage=60", + "Vary": ["Accept", "Accept-Encoding"], + "ETag": "W/\"a466888c00a5eaf3354ccabe37f75dd9\"", + "X-GitHub-Media-Type": "github.v3; format=json", + "Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", + "Access-Control-Allow-Origin": "*", + "Content-Security-Policy": "default-src 'none'", + "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "deny", + "X-XSS-Protection": "1; mode=block", + "X-Served-By": "88531cdcf1929112ec480e1806d44a33", + "X-GitHub-Request-Id": "BC8D23FA:31E4:269B4E33:5846F623" + } + } +}