diff --git a/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java b/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java index 1504297965..068401d95e 100644 --- a/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java +++ b/src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java @@ -170,7 +170,7 @@ public abstract class AbstractGitSCMSource extends SCMSource { public AbstractGitSCMSource() { } - + @Deprecated public AbstractGitSCMSource(String id) { setId(id); @@ -635,7 +635,9 @@ public Void run(GitClient client, String remoteName, FetchCommand fetch) throws discoverBranches(repository, walk, request, remoteReferences); } if (context.wantTags()) { - discoverTags(repository, walk, request, remoteReferences); + discoverTags(repository, walk, request, remoteReferences, + context.getAtLeastTagCommitTimeMillis(), + context.getAtMostTagCommitTimeMillis()); } if (context.wantOtherRefs()) { discoverOtherRefs(repository, walk, request, remoteReferences, @@ -775,7 +777,9 @@ public void record(@NonNull SCMHead head, SCMRevision revision, boolean isMatch) private void discoverTags(final Repository repository, final RevWalk walk, GitSCMSourceRequest request, - Map remoteReferences) + Map remoteReferences, + long atLeastMillis, + long atMostMillis) throws IOException, InterruptedException { listener.getLogger().println("Checking tags..."); walk.setRetainBody(false); @@ -788,6 +792,27 @@ private void discoverTags(final Repository repository, final String tagName = StringUtils.removeStart(ref.getKey(), Constants.R_TAGS); RevCommit commit = walk.parseCommit(ref.getValue()); final long lastModified = TimeUnit.SECONDS.toMillis(commit.getCommitTime()); + + if (atLeastMillis >= 0L || atMostMillis >= 0L) { + if (atMostMillis >= 0L && atLeastMillis > atMostMillis) { + /* Invalid. It's impossible for any tag to satisfy this. */ + listener.getLogger().format(" Skipping tag %s: invalid age range (min > max)%n", tagName); + continue; + } + long tagAgeMillis = System.currentTimeMillis() - lastModified; + long tagAgeDays = TimeUnit.MILLISECONDS.toDays(tagAgeMillis); + if (atMostMillis >= 0L && tagAgeMillis > atMostMillis) { + listener.getLogger().format(" Skipping tag %s: too old (%d days, max %d days)%n", + tagName, tagAgeDays, TimeUnit.MILLISECONDS.toDays(atMostMillis)); + continue; + } + if (atLeastMillis >= 0L && tagAgeMillis < atLeastMillis) { + listener.getLogger().format(" Skipping tag %s: too new (%d days, min %d days)%n", + tagName, tagAgeDays, TimeUnit.MILLISECONDS.toDays(atLeastMillis)); + continue; + } + } + if (request.process(new GitTagSCMHead(tagName, lastModified), new SCMSourceRequest.IntermediateLambda() { @Nullable diff --git a/src/main/java/jenkins/plugins/git/GitSCMSourceContext.java b/src/main/java/jenkins/plugins/git/GitSCMSourceContext.java index df04fdd288..ec6181a9f5 100644 --- a/src/main/java/jenkins/plugins/git/GitSCMSourceContext.java +++ b/src/main/java/jenkins/plugins/git/GitSCMSourceContext.java @@ -37,6 +37,7 @@ import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -95,6 +96,10 @@ public class GitSCMSourceContext, R extends @NonNull private String remoteName = AbstractGitSCMSource.DEFAULT_REMOTE_NAME; + private long atLeastTagCommitTimeMillis = -1L; + + private long atMostTagCommitTimeMillis = -1L; + /** * Constructor. * @@ -192,6 +197,14 @@ public final String remoteName() { return remoteName; } + public final long getAtLeastTagCommitTimeMillis() { + return atLeastTagCommitTimeMillis; + } + + public final long getAtMostTagCommitTimeMillis() { + return atMostTagCommitTimeMillis; + } + /** * Adds a requirement for branch details to any {@link GitSCMSourceRequest} for this context. * @@ -358,6 +371,29 @@ public final List asRefSpecs() { return result; } + private long getTagCommitTimeLimitMillisFromDays(String limitDays) { + try { + long tagCommitTimeLimit = Long.parseLong(StringUtils.defaultIfBlank(limitDays, "-1")); + return tagCommitTimeLimit < 0 ? -1L : TimeUnit.DAYS.toMillis(tagCommitTimeLimit); + } catch (NumberFormatException e) { + return -1L; + } + } + + @SuppressWarnings("unchecked") + @NonNull + public final C withAtLeastTagCommitTimeDays(String atLeastDays) { + this.atLeastTagCommitTimeMillis = getTagCommitTimeLimitMillisFromDays(atLeastDays); + return (C) this; + } + + @SuppressWarnings("unchecked") + @NonNull + public final C withAtMostTagCommitTimeDays(String atMostDays) { + this.atMostTagCommitTimeMillis = getTagCommitTimeLimitMillisFromDays(atMostDays); + return (C) this; + } + /** * {@inheritDoc} */ diff --git a/src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java b/src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java index 9c9be145a9..8eafcac735 100644 --- a/src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java +++ b/src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java @@ -23,6 +23,7 @@ */ package jenkins.plugins.git.traits; +import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import jenkins.plugins.git.GitSCMBuilder; @@ -51,11 +52,20 @@ * @since 3.6.0 */ public class TagDiscoveryTrait extends SCMSourceTrait { + private final String atLeastDays; + private final String atMostDays; + /** * Constructor for stapler. */ @DataBoundConstructor + public TagDiscoveryTrait(@CheckForNull String atLeastDays, @CheckForNull String atMostDays) { + this.atLeastDays = atLeastDays; + this.atMostDays = atMostDays; + } + public TagDiscoveryTrait() { + this(null, null); } /** @@ -66,6 +76,8 @@ protected void decorateContext(SCMSourceContext context) { GitSCMSourceContext ctx = (GitSCMSourceContext) context; ctx.wantTags(true); ctx.withAuthority(new TagSCMHeadAuthority()); + ctx.withAtLeastTagCommitTimeDays(atLeastDays) + .withAtMostTagCommitTimeDays(atMostDays); } /** @@ -76,6 +88,14 @@ public boolean includeCategory(@NonNull SCMHeadCategory category) { return category instanceof TagSCMHeadCategory; } + public String getAtLeastDays() { + return atLeastDays; + } + + public String getAtMostDays() { + return atMostDays; + } + /** * Our descriptor. */ diff --git a/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/config.jelly b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/config.jelly index 92acdaa269..a39235b14e 100644 --- a/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/config.jelly +++ b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/config.jelly @@ -1,4 +1,10 @@ + + + + + + diff --git a/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help-atLeastDays.html b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help-atLeastDays.html new file mode 100644 index 0000000000..9bef6280f8 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help-atLeastDays.html @@ -0,0 +1,15 @@ +
+ Minimum age of tags (in days) to include when discovering tags. + Tags newer than this value are ignored. + Tag age is calculated from the tag creation / commit timestamp to the time the + scan runs. + Leave this field blank to not filter out tags based on a minimum age + (no lower age bound). + Examples: +
    +
  • 7 – ignore tags created in the last 7 days (only tags at least 7 days old are used).
  • +
  • 0 – do not ignore tags for being too new (equivalent to leaving it blank).
  • +
+ When used together with Ignore tags older than (atMostDays), + this value should be less than or equal to the older than value to define a valid age range. +
diff --git a/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help-atMostDays.html b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help-atMostDays.html new file mode 100644 index 0000000000..638c243129 --- /dev/null +++ b/src/main/resources/jenkins/plugins/git/traits/TagDiscoveryTrait/help-atMostDays.html @@ -0,0 +1,16 @@ +
+ Maximum age of tags (in days) to include when discovering tags. + Tags older than this value are ignored. + Tag age is calculated from the tag creation / commit timestamp to the time the + scan runs. + Leave this field blank to not filter out tags based on a maximum age + (no upper age bound). + Examples: +
    +
  • 7 – ignore tags older than 7 days (only tags from the last 7 days are used).
  • +
  • 30 – ignore tags older than 30 days (only tags from the last 30 days are used).
  • +
+ When used together with Ignore tags newer than (atLeastDays), + this value should be greater than or equal to the newer than value so that the + age range is consistent. +
diff --git a/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java index 1524ebab11..9f0a164da5 100644 --- a/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java +++ b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java @@ -36,6 +36,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.UUID; +import java.util.stream.Collectors; import jenkins.plugins.git.junit.jupiter.WithGitSampleRepo; import jenkins.plugins.git.traits.BranchDiscoveryTrait; @@ -291,6 +292,34 @@ void retrieveHeadsSupportsTagDiscovery_onlyTagsWithoutBranchDiscoveryTrait() thr assertEquals("[SCMHead{'annotated'}, SCMHead{'lightweight'}]", source.fetch(listener).toString()); } + @Issue("JENKINS-64810") + @Test + void retrieveHeadsSupportsTagDiscovery_withTagAgeRestrictions() throws Exception { + assumeTrue(isTimeAvailable(), "Test class max time " + MAX_SECONDS_FOR_THESE_TESTS + " exceeded"); + sampleRepo.init(); + sampleRepo.write("file", "initial"); + sampleRepo.git("commit", "--all", "--message=initial"); + sampleRepo.git("tag", "test-tag"); + + GitSCMSource source = new GitSCMSource(sampleRepo.toString()); + TaskListener listener = StreamTaskListener.fromStderr(); + + source.setTraits(Collections.singletonList(new TagDiscoveryTrait())); + assertThat(source.fetch(listener).stream().map(SCMHead::getName).collect(Collectors.toSet()), contains("test-tag")); + + source.setTraits(Collections.singletonList(new TagDiscoveryTrait(null, "0"))); + assertThat(source.fetch(listener).stream().map(SCMHead::getName).collect(Collectors.toSet()), not(contains("test-tag"))); + + source.setTraits(Collections.singletonList(new TagDiscoveryTrait("1", null))); + assertThat(source.fetch(listener).stream().map(SCMHead::getName).collect(Collectors.toSet()), not(contains("test-tag"))); + + source.setTraits(Collections.singletonList(new TagDiscoveryTrait("1", "0"))); + assertThat(source.fetch(listener), empty()); + + source.setTraits(Collections.singletonList(new TagDiscoveryTrait("apple", "banana"))); + assertThat(source.fetch(listener).stream().map(SCMHead::getName).collect(Collectors.toSet()), contains("test-tag")); + } + @Issue("JENKINS-45953") @Test void retrieveRevisions() throws Exception {