Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@

public AbstractGitSCMSource() {
}

@Deprecated
public AbstractGitSCMSource(String id) {
setId(id);
Expand Down Expand Up @@ -635,7 +635,9 @@
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,
Expand Down Expand Up @@ -775,7 +777,9 @@

private void discoverTags(final Repository repository,
final RevWalk walk, GitSCMSourceRequest request,
Map<String, ObjectId> remoteReferences)
Map<String, ObjectId> remoteReferences,
long atLeastMillis,
long atMostMillis)
throws IOException, InterruptedException {
listener.getLogger().println("Checking tags...");
walk.setRetainBody(false);
Expand All @@ -788,6 +792,27 @@
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;
}
Comment on lines 796 to 801
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering logic checks if atMostMillis >= 0L && atLeastMillis > atMostMillis and continues to skip the tag if this invalid configuration is detected. However, this check is performed inside the loop for every tag, which is inefficient.

Consider moving this validation earlier, such as:

  1. In the getTagCommitTimeLimitMillisFromDays method or the withAtLeast/withAtMost methods in GitSCMSourceContext
  2. At the beginning of the discoverTags method before the loop

This would provide earlier feedback to users about configuration errors and avoid redundant checks for every tag.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary.

long tagAgeMillis = System.currentTimeMillis() - lastModified;
long tagAgeDays = TimeUnit.MILLISECONDS.toDays(tagAgeMillis);
if (atMostMillis >= 0L && tagAgeMillis > atMostMillis) {

Check warning on line 804 in src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 804 is only partially covered, one branch is missing
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) {

Check warning on line 809 in src/main/java/jenkins/plugins/git/AbstractGitSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 809 is only partially covered, 2 branches are missing
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<ObjectId>() {
@Nullable
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/jenkins/plugins/git/GitSCMSourceContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -95,6 +96,10 @@ public class GitSCMSourceContext<C extends GitSCMSourceContext<C, R>, R extends
@NonNull
private String remoteName = AbstractGitSCMSource.DEFAULT_REMOTE_NAME;

private long atLeastTagCommitTimeMillis = -1L;

private long atMostTagCommitTimeMillis = -1L;

/**
* Constructor.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -358,6 +371,29 @@ public final List<RefSpec> 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}
*/
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -66,6 +76,8 @@
GitSCMSourceContext<?,?> ctx = (GitSCMSourceContext<?, ?>) context;
ctx.wantTags(true);
ctx.withAuthority(new TagSCMHeadAuthority());
ctx.withAtLeastTagCommitTimeDays(atLeastDays)
.withAtMostTagCommitTimeDays(atMostDays);
}
Comment on lines +79 to 81
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR adds new functionality (time-based tag filtering) but mentions "None yet" for testing. The AbstractGitSCMSourceTest file already has comprehensive test coverage for TagDiscoveryTrait (as seen in tests like retrieveHeadsSupportsTagDiscovery_findTagsWithTagDiscoveryTrait), but there are no tests for the new time-based filtering parameters.

Tests should be added to verify:

  1. Tags are correctly filtered when atLeastDays is set (tags newer than threshold are excluded)
  2. Tags are correctly filtered when atMostDays is set (tags older than threshold are excluded)
  3. Both parameters work together correctly
  4. Edge case: when atLeastDays > atMostDays (invalid configuration), no tags are discovered
  5. Null/empty values are handled correctly (no filtering applied)
  6. Invalid input (non-numeric strings) is handled gracefully

This is important to ensure the feature works as expected and to prevent regressions.

Copilot uses AI. Check for mistakes.

/**
Expand All @@ -76,6 +88,14 @@
return category instanceof TagSCMHeadCategory;
}

public String getAtLeastDays() {
return atLeastDays;
}

public String getAtMostDays() {
return atMostDays;

Check warning on line 96 in src/main/java/jenkins/plugins/git/traits/TagDiscoveryTrait.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 92-96 are not covered by tests
}

/**
* Our descriptor.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:c="/lib/credentials"
xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry field="atLeastDays" title="${%Ignore tags newer than}">
<f:number min="0"/>
</f:entry>
<f:entry field="atMostDays" title="${%Ignore tags older than}">
Comment on lines 4 to 7
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI field titles "Ignore tags newer than" and "Ignore tags older than" use double negatives that can be confusing. The field names are "atLeastDays" and "atMostDays" which refer to tag age requirements, but the labels use "ignore" which inverts the logic.

Consider more intuitive labels:

  • For atLeastDays: "Minimum tag age (days)" or "Only discover tags at least this many days old"
  • For atMostDays: "Maximum tag age (days)" or "Only discover tags at most this many days old"

Alternatively, use positive framing:

  • For atLeastDays: "Discover tags older than (days)"
  • For atMostDays: "Discover tags newer than (days)"

This would make the UI clearer and reduce the cognitive load on users trying to configure the right values.

Suggested change
<f:entry field="atLeastDays" title="${%Ignore tags newer than}">
<f:number default=""/>
</f:entry>
<f:entry field="atMostDays" title="${%Ignore tags older than}">
<f:entry field="atLeastDays" title="${%Discover tags older than (days)}">
<f:number default=""/>
</f:entry>
<f:entry field="atMostDays" title="${%Discover tags newer than (days)}">

Copilot uses AI. Check for mistakes.
<f:number min="0"/>
</f:entry>
Comment on lines 4 to 9
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field order in the UI might be counterintuitive. The "atLeastDays" field (tags older than X days) appears before "atMostDays" field (tags newer than Y days). When users think about filtering by time range, they typically think in chronological order: "from X days ago to Y days ago" or in age order: "minimum age to maximum age".

Consider reordering the fields to be more intuitive:

  1. If using the current labels, swap them so "Ignore tags older than" (atMostDays) comes first, then "Ignore tags newer than" (atLeastDays)
  2. Or rename to "Minimum tag age" (atLeastDays) first, then "Maximum tag age" (atMostDays)

This would align better with user expectations when setting up a time range filter.

Suggested change
<f:entry field="atLeastDays" title="${%Ignore tags newer than}">
<f:number default=""/>
</f:entry>
<f:entry field="atMostDays" title="${%Ignore tags older than}">
<f:number default="7"/>
</f:entry>
<f:entry field="atMostDays" title="${%Ignore tags older than}">
<f:number default="7"/>
</f:entry>
<f:entry field="atLeastDays" title="${%Ignore tags newer than}">
<f:number default=""/>
</f:entry>

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt this.

</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div>
Minimum age of tags (in days) to <strong>include</strong> 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 <strong>not</strong> filter out tags based on a minimum age
(no lower age bound).
Examples:
<ul>
<li><code>7</code> – ignore tags created in the last 7 days (only tags at least 7 days old are used).</li>
<li><code>0</code> – do not ignore tags for being too new (equivalent to leaving it blank).</li>
</ul>
When used together with <em>Ignore tags older than</em> (<code>atMostDays</code>),
this value should be less than or equal to the <em>older than</em> value to define a valid age range.
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div>
Maximum age of tags (in days) to <strong>include</strong> 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 <strong>not</strong> filter out tags based on a maximum age
(no upper age bound).
Examples:
<ul>
<li><code>7</code> – ignore tags older than 7 days (only tags from the last 7 days are used).</li>
<li><code>30</code> – ignore tags older than 30 days (only tags from the last 30 days are used).</li>
</ul>
When used together with <em>Ignore tags newer than</em> (<code>atLeastDays</code>),
this value should be greater than or equal to the <em>newer than</em> value so that the
age range is consistent.
</div>
29 changes: 29 additions & 0 deletions src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Loading