From b47201dbf89e587b524fd2c28af351b7904783b5 Mon Sep 17 00:00:00 2001 From: Ulrich Grave Date: Sat, 15 Nov 2025 16:49:07 +0100 Subject: [PATCH] Add trait to discard old tags/branches from discovering --- .../DiscardOldBranchTrait.java | 81 ++++++++++++++++++ .../DiscardOldTagTrait.java | 78 ++++++++++++++++++ .../DiscardOldBranchTrait/config.jelly | 6 ++ .../help-keepForDays.html | 3 + .../DiscardOldBranchTrait/help.html | 3 + .../DiscardOldTagTrait/config.jelly | 6 ++ .../DiscardOldTagTrait/help-keepForDays.html | 3 + .../DiscardOldTagTrait/help.html | 3 + .../gitlabbranchsource/Messages.properties | 2 + .../DiscardOldBranchTraitTest.java | 82 +++++++++++++++++++ .../DiscardOldTagTraitTest.java | 53 ++++++++++++ 11 files changed, 320 insertions(+) create mode 100644 src/main/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait.java create mode 100644 src/main/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait.java create mode 100644 src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/config.jelly create mode 100644 src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/help-keepForDays.html create mode 100644 src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/help.html create mode 100644 src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/config.jelly create mode 100644 src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/help-keepForDays.html create mode 100644 src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/help.html create mode 100644 src/test/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTraitTest.java create mode 100644 src/test/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTraitTest.java diff --git a/src/main/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait.java b/src/main/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait.java new file mode 100644 index 00000000..361c56fd --- /dev/null +++ b/src/main/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait.java @@ -0,0 +1,81 @@ +package io.jenkins.plugins.gitlabbranchsource; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import java.time.LocalDate; +import java.time.ZoneOffset; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.trait.SCMHeadFilter; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceRequest; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.gitlab4j.api.models.Branch; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Discard all branches with head commit older than the configured days. + */ +public class DiscardOldBranchTrait extends SCMSourceTrait { + + private int keepForDays = 1; + + @DataBoundConstructor + public DiscardOldBranchTrait(int keepForDays) { + this.keepForDays = keepForDays; + } + + public int getKeepForDays() { + return keepForDays; + } + + @Override + protected void decorateContext(SCMSourceContext context) { + context.withFilter(new ExcludeOldSCMHeadBranch(keepForDays)); + } + + static final class ExcludeOldSCMHeadBranch extends SCMHeadFilter { + + private final int keepForDays; + + public ExcludeOldSCMHeadBranch(int keepForDays) { + this.keepForDays = keepForDays; + } + + @Override + public boolean isExcluded(@NonNull SCMSourceRequest request, @NonNull SCMHead head) { + GitLabSCMSourceRequest glRequest = (GitLabSCMSourceRequest) request; + String branchName = head.getName(); + if (head instanceof MergeRequestSCMHead mrHead) { + branchName = mrHead.getOriginName(); + } + + for (Branch branch : glRequest.getBranches()) { + if (branchName.equals(branch.getName())) { + LocalDate commitDate = LocalDate.ofInstant( + branch.getCommit().getCommittedDate().toInstant(), ZoneOffset.UTC); + LocalDate expiryDate = LocalDate.now(ZoneOffset.UTC).minusDays(keepForDays); + return commitDate.isBefore(expiryDate); + } + } + return false; + } + } + + @Symbol("gitLabDiscardOldBranch") + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + @NonNull + @Override + public String getDisplayName() { + return Messages.DiscardOldBranchTrait_displayName(); + } + + @Override + public Class getContextClass() { + return GitLabSCMSourceContext.class; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait.java b/src/main/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait.java new file mode 100644 index 00000000..ed6c56a8 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait.java @@ -0,0 +1,78 @@ +package io.jenkins.plugins.gitlabbranchsource; + +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import java.time.LocalDate; +import java.time.ZoneOffset; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMSource; +import jenkins.scm.api.mixin.TagSCMHead; +import jenkins.scm.api.trait.SCMHeadPrefilter; +import jenkins.scm.api.trait.SCMSourceContext; +import jenkins.scm.api.trait.SCMSourceTrait; +import jenkins.scm.api.trait.SCMSourceTraitDescriptor; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Discard all tags with creation date older than the configured days. + */ +public class DiscardOldTagTrait extends SCMSourceTrait { + + private int keepForDays = 1; + + @DataBoundConstructor + public DiscardOldTagTrait(int keepForDays) { + this.keepForDays = keepForDays; + } + + public int getKeepForDays() { + return keepForDays; + } + + @Override + protected void decorateContext(SCMSourceContext context) { + context.withPrefilter(new ExcludeOldSCMTag(keepForDays)); + } + + static final class ExcludeOldSCMTag extends SCMHeadPrefilter { + + private final int keepForDays; + + public ExcludeOldSCMTag(int keepForDays) { + this.keepForDays = keepForDays; + } + + @Override + public boolean isExcluded(@NonNull SCMSource source, @NonNull SCMHead head) { + if (!(head instanceof TagSCMHead tagHead) || tagHead.getTimestamp() == 0) { + return false; + } + + LocalDate commitDate = asLocalDate(tagHead.getTimestamp()); + LocalDate expiryDate = LocalDate.now(ZoneOffset.UTC).minusDays(keepForDays); + return commitDate.isBefore(expiryDate); + } + + @NonNull + private LocalDate asLocalDate(long milliseconds) { + return new java.sql.Date(milliseconds).toLocalDate(); + } + } + + @Symbol("gitLabDiscardOldTag") + @Extension + public static class DescriptorImpl extends SCMSourceTraitDescriptor { + + @NonNull + @Override + public String getDisplayName() { + return Messages.DiscardOldTagTrait_displayName(); + } + + @Override + public Class getContextClass() { + return GitLabSCMSourceContext.class; + } + } +} diff --git a/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/config.jelly b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/config.jelly new file mode 100644 index 00000000..572032c6 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/help-keepForDays.html b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/help-keepForDays.html new file mode 100644 index 00000000..13f337bf --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/help-keepForDays.html @@ -0,0 +1,3 @@ +
+ Number of days since the last commit in the branch before it is discarded. +
diff --git a/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/help.html b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/help.html new file mode 100644 index 00000000..d164cf58 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTrait/help.html @@ -0,0 +1,3 @@ +
+ Discard all branches with head commit older than the configured days. +
diff --git a/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/config.jelly b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/config.jelly new file mode 100644 index 00000000..572032c6 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/config.jelly @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/help-keepForDays.html b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/help-keepForDays.html new file mode 100644 index 00000000..86be7a53 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/help-keepForDays.html @@ -0,0 +1,3 @@ +
+ Number of days since the commit date referred by the tag before it is discarded. +
diff --git a/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/help.html b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/help.html new file mode 100644 index 00000000..9eabbe80 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTrait/help.html @@ -0,0 +1,3 @@ +
+ Discard all tags created before the configured days. +
diff --git a/src/main/resources/io/jenkins/plugins/gitlabbranchsource/Messages.properties b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/Messages.properties index 633c68ee..074ef66e 100644 --- a/src/main/resources/io/jenkins/plugins/gitlabbranchsource/Messages.properties +++ b/src/main/resources/io/jenkins/plugins/gitlabbranchsource/Messages.properties @@ -62,3 +62,5 @@ GitLabWebHookCause.ShortDescription.Push=Started by GitLab push by {0} GitLabWebHookCause.ShortDescription.MergeRequestHook=Triggered by GitLab Merge Request #{0}: {1} => {2} WebhookListenerBuildConditionsTrait.displayName=Webhook Listener Conditions GitLabMarkUnstableAsSuccessTrait.displayName=Mark unstable build as successful on Gitlab +DiscardOldBranchTrait_displayName=Discard branch older than given days +DiscardOldTagTrait_displayName=Discard tag older than given days diff --git a/src/test/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTraitTest.java b/src/test/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTraitTest.java new file mode 100644 index 00000000..33e03ce7 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldBranchTraitTest.java @@ -0,0 +1,82 @@ +package io.jenkins.plugins.gitlabbranchsource; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.jenkins.plugins.gitlabbranchsource.DiscardOldBranchTrait.ExcludeOldSCMHeadBranch; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.trait.SCMHeadFilter; +import org.apache.commons.lang.time.DateUtils; +import org.gitlab4j.api.models.Branch; +import org.gitlab4j.api.models.Commit; +import org.junit.jupiter.api.Test; + +class DiscardOldBranchTraitTest { + + @Test + void should_include_branch_if_last_commit_within_range() throws Exception { + DiscardOldBranchTrait uut = new DiscardOldBranchTrait(10); + GitLabSCMSourceContext context = new GitLabSCMSourceContext(null, SCMHeadObserver.none()); + uut.decorateContext(context); + + Optional optFilter = context.filters().stream() + .filter(it -> ExcludeOldSCMHeadBranch.class.equals(it.getClass())) + .findFirst(); + assertTrue(optFilter.isPresent()); + + SCMHead head = mock(SCMHead.class); + when(head.getName()).thenReturn("expected"); + + Date today = new Date(); + + GitLabSCMSourceRequest request = mock(GitLabSCMSourceRequest.class); + when(request.getBranches()) + .thenReturn(List.of( + buildBranch("other", DateUtils.addDays(today, -7)), + buildBranch("expected", DateUtils.addDays(today, -10)))); + + SCMHeadFilter filter = optFilter.get(); + assertFalse(filter.isExcluded(request, head)); + } + + @Test + void should_exclude_branch_if_last_commit_not_within_range() throws Exception { + DiscardOldBranchTrait uut = new DiscardOldBranchTrait(10); + GitLabSCMSourceContext context = new GitLabSCMSourceContext(null, SCMHeadObserver.none()); + uut.decorateContext(context); + + Optional optFilter = context.filters().stream() + .filter(it -> ExcludeOldSCMHeadBranch.class.equals(it.getClass())) + .findFirst(); + assertTrue(optFilter.isPresent()); + + SCMHead head = mock(SCMHead.class); + when(head.getName()).thenReturn("expected"); + + Date today = new Date(); + + GitLabSCMSourceRequest request = mock(GitLabSCMSourceRequest.class); + when(request.getBranches()) + .thenReturn(List.of( + buildBranch("other", DateUtils.addDays(today, -7)), + buildBranch("expected", DateUtils.addDays(today, -11)))); + + SCMHeadFilter filter = optFilter.get(); + assertTrue(filter.isExcluded(request, head)); + } + + private Branch buildBranch(String name, Date commitDate) { + Branch branch = new Branch(); + branch.setName(name); + Commit commit = new Commit(); + commit.setCommittedDate(commitDate); + branch.setCommit(commit); + return branch; + } +} diff --git a/src/test/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTraitTest.java b/src/test/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTraitTest.java new file mode 100644 index 00000000..d2499b87 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/gitlabbranchsource/DiscardOldTagTraitTest.java @@ -0,0 +1,53 @@ +package io.jenkins.plugins.gitlabbranchsource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.jenkins.plugins.gitlabbranchsource.DiscardOldTagTrait.ExcludeOldSCMTag; +import java.util.Date; +import java.util.Optional; +import java.util.stream.Stream; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMHeadObserver; +import jenkins.scm.api.trait.SCMHeadPrefilter; +import jenkins.scm.impl.NullSCMSource; +import org.apache.commons.lang3.time.DateUtils; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class DiscardOldTagTraitTest { + + static Stream tagSCMHeadProvider() { + return Stream.of( + Arguments.argumentSet( + "expired", + new GitLabTagSCMHead( + "tag/1234", DateUtils.addDays(new Date(), -6).getTime()), + true), + Arguments.argumentSet( + "too_recent", + new GitLabTagSCMHead( + "tag/1234", DateUtils.addDays(new Date(), -4).getTime()), + false), + Arguments.argumentSet("no_timestamp", new GitLabTagSCMHead("tag/zer0", 0L), false), + Arguments.argumentSet("not_a_tag", new BranchSCMHead("someBranch"), false)); + } + + @ParameterizedTest + @MethodSource("tagSCMHeadProvider") + void verify_tag_filtering(SCMHead head, boolean expectedResult) { + DiscardOldTagTrait uut = new DiscardOldTagTrait(5); + GitLabSCMSourceContext context = new GitLabSCMSourceContext(null, SCMHeadObserver.none()); + uut.decorateContext(context); + + Optional optFilter = context.prefilters().stream() + .filter(it -> ExcludeOldSCMTag.class.equals(it.getClass())) + .findFirst(); + assertTrue(optFilter.isPresent()); + + SCMHeadPrefilter filter = optFilter.get(); + + assertEquals(filter.isExcluded(new NullSCMSource(), head), expectedResult); + } +}