From 3dbab0dfca8e02f93ca3c4906a3ebd8275356309 Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Mon, 7 Jul 2025 14:47:01 +0200 Subject: [PATCH 1/3] Add UI elements for partial clone --- .../extensions/impl/PartialCloneFilter.java | 110 ++++++++++++++++++ .../git/traits/PartialCloneFilterTrait.java | 41 +++++++ .../impl/PartialCloneFilter/config.groovy | 7 ++ .../impl/PartialCloneFilter/help.html | 4 + .../plugins/git/AbstractGitSCMSourceTest.java | 5 + .../git/AbstractGitSCMSourceWantTagsTest.java | 5 + 6 files changed, 172 insertions(+) create mode 100644 src/main/java/hudson/plugins/git/extensions/impl/PartialCloneFilter.java create mode 100644 src/main/java/jenkins/plugins/git/traits/PartialCloneFilterTrait.java create mode 100644 src/main/resources/hudson/plugins/git/extensions/impl/PartialCloneFilter/config.groovy create mode 100644 src/main/resources/hudson/plugins/git/extensions/impl/PartialCloneFilter/help.html diff --git a/src/main/java/hudson/plugins/git/extensions/impl/PartialCloneFilter.java b/src/main/java/hudson/plugins/git/extensions/impl/PartialCloneFilter.java new file mode 100644 index 0000000000..5aa6df37ce --- /dev/null +++ b/src/main/java/hudson/plugins/git/extensions/impl/PartialCloneFilter.java @@ -0,0 +1,110 @@ +package hudson.plugins.git.extensions.impl; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.Extension; +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.extensions.GitClientType; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; +import java.io.IOException; +import java.util.Objects; +import org.jenkinsci.Symbol; +import org.jenkinsci.plugins.gitclient.CloneCommand; +import org.jenkinsci.plugins.gitclient.FetchCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted; +import org.kohsuke.stapler.DataBoundConstructor; + +public class PartialCloneFilter extends GitSCMExtension { + + private final String filterSpec; + + @DataBoundConstructor + public PartialCloneFilter(String filterSpec) { + this.filterSpec = filterSpec; + } + + @Whitelisted + public String getFilterSpec() { + return filterSpec; + } + + /** + * {@inheritDoc} + */ + @Override + public void decorateCloneCommand( + GitSCM scm, Run build, GitClient git, TaskListener listener, CloneCommand cmd) + throws IOException, InterruptedException, GitException { + + cmd.filter(filterSpec); + } + + /** + * {@inheritDoc} + */ + @Override + public void decorateFetchCommand( + GitSCM scm, @CheckForNull Run run, GitClient git, TaskListener listener, FetchCommand cmd) + throws IOException, InterruptedException, GitException { + + cmd.filter(filterSpec); + } + + /** + * {@inheritDoc} + */ + @Override + public GitClientType getRequiredClient() { + return GitClientType.GITCLI; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PartialCloneFilter that = (PartialCloneFilter) o; + + return Objects.equals(filterSpec, that.filterSpec); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(filterSpec); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return "PartialCloneFilter{" + "filterSpec=" + filterSpec + '}'; + } + + @Extension + @Symbol("partialCloneFilter") + public static class DescriptorImpl extends GitSCMExtensionDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Partial clone"; + } + } +} diff --git a/src/main/java/jenkins/plugins/git/traits/PartialCloneFilterTrait.java b/src/main/java/jenkins/plugins/git/traits/PartialCloneFilterTrait.java new file mode 100644 index 0000000000..20de1af8af --- /dev/null +++ b/src/main/java/jenkins/plugins/git/traits/PartialCloneFilterTrait.java @@ -0,0 +1,41 @@ +package jenkins.plugins.git.traits; + +import hudson.Extension; +import hudson.plugins.git.extensions.impl.PartialCloneFilter; +import jenkins.scm.api.trait.SCMSourceTrait; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Exposes {@link PartialCloneFilter} as a {@link SCMSourceTrait}. + * + * @since 6.3.0 + */ +public class PartialCloneFilterTrait extends GitSCMExtensionTrait { + + /** + * Stapler constructor. + * + * @param extension the {@link PartialCloneFilter} + */ + @DataBoundConstructor + public PartialCloneFilterTrait(PartialCloneFilter extension) { + super(extension); + } + + /** + * Our {@link hudson.model.Descriptor} + */ + @Extension + @Symbol("partialCloneFilter") + public static class DescriptorImpl extends GitSCMExtensionTraitDescriptor { + + /** + * {@inheritDoc} + */ + @Override + public String getDisplayName() { + return "Partial clone"; + } + } +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PartialCloneFilter/config.groovy b/src/main/resources/hudson/plugins/git/extensions/impl/PartialCloneFilter/config.groovy new file mode 100644 index 0000000000..6383965ce9 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PartialCloneFilter/config.groovy @@ -0,0 +1,7 @@ +package hudson.plugins.git.extensions.impl.PartialCloneFilter + +def f = namespace(lib.FormTagLib) + +f.entry(title:_("Filter specification"), field:"filterSpec") { + f.textbox() +} diff --git a/src/main/resources/hudson/plugins/git/extensions/impl/PartialCloneFilter/help.html b/src/main/resources/hudson/plugins/git/extensions/impl/PartialCloneFilter/help.html new file mode 100644 index 0000000000..9614997740 --- /dev/null +++ b/src/main/resources/hudson/plugins/git/extensions/impl/PartialCloneFilter/help.html @@ -0,0 +1,4 @@ +
+ Specify an object filter for clone and fetch.
+ If set, the repository is converted to a partial clone. +
diff --git a/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java index 1524ebab11..63677fb441 100644 --- a/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java +++ b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceTest.java @@ -1221,6 +1221,11 @@ public FetchCommand depth(Integer integer) { return this; } + @Override + public FetchCommand filter(String filterSpec) { + return this; + } + @Override public void execute() throws GitException, InterruptedException { fetchCommand.execute(); diff --git a/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceWantTagsTest.java b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceWantTagsTest.java index 2a7b87ca76..17cdf37e66 100644 --- a/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceWantTagsTest.java +++ b/src/test/java/jenkins/plugins/git/AbstractGitSCMSourceWantTagsTest.java @@ -228,6 +228,11 @@ public FetchCommand depth(Integer integer) { return this; } + @Override + public FetchCommand filter(String filterSpec) { + return this; + } + @Override public void execute() throws GitException, InterruptedException { fetchCommand.execute(); From f1edd825f495ce54832c2e04af3f89bb8e361121 Mon Sep 17 00:00:00 2001 From: Thomas Duboucher Date: Wed, 3 Jun 2026 13:12:26 +0200 Subject: [PATCH 2/3] Add UI tests for partial clone --- pom.xml | 5 +++-- .../hudson/plugins/git/CredentialsUserRemoteConfigTest.java | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0b956e2623..39bd9c4827 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ jenkinsci/${project.artifactId}-plugin true - 2.504 + 2.516 ${jenkins.baseline}.3 false @@ -55,7 +55,7 @@ io.jenkins.tools.bom bom-${jenkins.baseline}.x - 5983.v443959746f1f + 6210.v69ea_fd8a_f010 pom import @@ -69,6 +69,7 @@ org.jenkins-ci.plugins git-client + 6.6.2-SNAPSHOT org.jenkins-ci.plugins diff --git a/src/test/java/hudson/plugins/git/CredentialsUserRemoteConfigTest.java b/src/test/java/hudson/plugins/git/CredentialsUserRemoteConfigTest.java index 1027e2f773..4b20dca986 100644 --- a/src/test/java/hudson/plugins/git/CredentialsUserRemoteConfigTest.java +++ b/src/test/java/hudson/plugins/git/CredentialsUserRemoteConfigTest.java @@ -114,6 +114,7 @@ private String randomPipelineExtensions() { // Inverse build chooser will find nothing to build and fails the test // "[$class: 'BuildChooserSetting', buildChooser: [$class: 'InverseBuildChooser']]", "[$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [[path: 'src'], [path: 'Makefile']]]", + "[$class: 'PartialCloneFilter', filterSpec: 'blob:none'", "[$class: 'AuthorInChangelog']", "[$class: 'BuildChooserSetting', buildChooser: [$class: 'DefaultBuildChooser']]", "[$class: 'BuildSingleRevisionOnly']", @@ -160,6 +161,7 @@ private String randomPipelineExtensions() { "pruneTags(false)", "pruneTags(true)", "sparseCheckout(sparseCheckoutPaths: [[path: 'src'], [path: 'Makefile']])", + "partialCloneFilter('blob:none')", "submodule(disableSubmodules: true)", "submodule(depth: 1, shallow: true)", "submodule(parentCredentials: true, recursiveSubmodules: true, threads: 13)", From c3c4cbba0eaa0d430fd7c69a562e4438bfe961fd Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Mon, 8 Jun 2026 17:20:19 +0200 Subject: [PATCH 3/3] [WIP] Add UI test --- .../impl/PartialCloneFilterTest.java | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 src/test/java/hudson/plugins/git/extensions/impl/PartialCloneFilterTest.java diff --git a/src/test/java/hudson/plugins/git/extensions/impl/PartialCloneFilterTest.java b/src/test/java/hudson/plugins/git/extensions/impl/PartialCloneFilterTest.java new file mode 100644 index 0000000000..47950d91f0 --- /dev/null +++ b/src/test/java/hudson/plugins/git/extensions/impl/PartialCloneFilterTest.java @@ -0,0 +1,211 @@ +package hudson.plugins.git.extensions.impl; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +import hudson.model.Run; +import hudson.model.TaskListener; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import java.util.List; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.URIish; +import org.jenkinsci.plugins.gitclient.CloneCommand; +import org.jenkinsci.plugins.gitclient.FetchCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PartialCloneFilterTest { + + private static final String BLOBLESS_FILTER_SPEC = "blob:none"; + + private PartialCloneFilter bloblessPartialCloneFilter; + + @BeforeEach + void beforeEach() { + bloblessPartialCloneFilter = new PartialCloneFilter(BLOBLESS_FILTER_SPEC); + } + + @Test + void testGetFilterSpec() { + assertThat(bloblessPartialCloneFilter.getFilterSpec(), is(BLOBLESS_FILTER_SPEC)); + } + + @Test + void testDecorateCloneCommand() throws Exception { + GitSCM scm = null; + Run build = null; + GitClient git = null; + TaskListener listener = null; + MyCloneCommand cmd = new MyCloneCommand(); + bloblessPartialCloneFilter.decorateCloneCommand(scm, build, git, listener, cmd); + assertThat(cmd.getFilterSpec(), is(BLOBLESS_FILTER_SPEC)); + } + + @Test + void testDecorateFetchCommand() throws Exception { + GitSCM scm = null; + Run build = null; + GitClient git = null; + TaskListener listener = null; + MyFetchCommand cmd = new MyFetchCommand(); + bloblessPartialCloneFilter.decorateFetchCommand(scm, build, git, listener, cmd); + assertThat(cmd.getFilterSpec(), is(BLOBLESS_FILTER_SPEC)); + } + + @Test + void equalsContract() { + EqualsVerifier.forClass(PartialCloneFilter.class).usingGetClass().verify(); + } + + @Test + void testHashCode() { + PartialCloneFilter bloblessPartialCloneFilterCopy = new PartialCloneFilter(BLOBLESS_FILTER_SPEC); + assertThat(bloblessPartialCloneFilter.hashCode(), is(bloblessPartialCloneFilterCopy.hashCode())); + assertThat(bloblessPartialCloneFilter, is(bloblessPartialCloneFilterCopy)); + } + + @Test + void testToString() { + assertThat( + bloblessPartialCloneFilter.toString(), + is("PartialCloneFilter{filterSpec=" + BLOBLESS_FILTER_SPEC + "}")); + } + + private static class MyCloneCommand implements CloneCommand { + + private String filterSpec; + + String getFilterSpec() { + return filterSpec; + } + + @Override + public CloneCommand url(String url) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand repositoryName(String name) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand shallow() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand shallow(boolean shallow) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand shared() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand shared(boolean shared) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand reference(String reference) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand timeout(Integer timeout) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand noCheckout() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand tags(boolean tags) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand refspecs(List refspecs) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand depth(Integer depth) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CloneCommand filter(String filterSpec) { + this.filterSpec = filterSpec; + return this; + } + + @Override + public void execute() { + throw new UnsupportedOperationException("Not supported yet."); + } + } + + private static class MyFetchCommand implements FetchCommand { + + private String filterSpec; + + String getFilterSpec() { + return filterSpec; + } + + @Override + public FetchCommand from(URIish remote, List refspecs) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FetchCommand prune() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FetchCommand prune(boolean prune) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FetchCommand shallow(boolean shallow) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FetchCommand timeout(Integer timeout) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FetchCommand tags(boolean tags) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FetchCommand depth(Integer depth) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public FetchCommand filter(String filterSpec) { + this.filterSpec = filterSpec; + return this; + } + + @Override + public void execute() throws GitException, InterruptedException { + throw new UnsupportedOperationException("Not supported yet."); + } + } +}