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 extends SCMSourceContext> 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 extends SCMSourceContext> 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);
+ }
+}