From ea1f4c6a7e14b229b6a8b14240836300eb233da7 Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Mon, 7 Jul 2025 13:19:08 +0200 Subject: [PATCH 01/10] Add filter() API to clone and fetch --- .../plugins/gitclient/CliGitAPIImpl.java | 25 +++++++++++++++++++ .../plugins/gitclient/CloneCommand.java | 8 ++++++ .../plugins/gitclient/FetchCommand.java | 8 ++++++ .../plugins/gitclient/JGitAPIImpl.java | 12 +++++++++ 4 files changed, 53 insertions(+) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index ff07100eed..a156786257 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -522,6 +522,7 @@ public FetchCommand fetch_() { private Integer timeout; private boolean tags = true; private Integer depth = 1; + private String filterSpec = null; @Override public FetchCommand from(URIish remote, List refspecs) { @@ -566,6 +567,12 @@ public FetchCommand depth(Integer depth) { return this; } + @Override + public FetchCommand filter(String filterSpec) { + this.filterSpec = filterSpec; + return this; + } + @Override public void execute() throws GitException, InterruptedException { listener.getLogger().println("Fetching upstream changes from " + url); @@ -592,6 +599,12 @@ public void execute() throws GitException, InterruptedException { args.add("--depth=" + depth); } + if (filterSpec != null && isAtLeastVersion(2, 22, 0, 0)) { + // in the future, we could add --refetch if we detect a change in the filter configuration to + // trigger maintenance + args.add("--filter=" + filterSpec); + } + warnIfWindowsTemporaryDirNameHasSpaces(); StandardCredentials cred = credentials.get(url.toPrivateString()); @@ -727,6 +740,7 @@ public CloneCommand clone_() { private boolean tags = true; private List refspecs; private Integer depth = 1; + private String filterSpec = null; @Override public CloneCommand url(String url) { @@ -802,6 +816,12 @@ public CloneCommand refspecs(List refspecs) { return this; } + @Override + public CloneCommand filter(String filterSpec) { + this.filterSpec = filterSpec; + return this; + } + @Override public void execute() throws GitException, InterruptedException { @@ -872,9 +892,14 @@ public void execute() throws GitException, InterruptedException { if (refspecs == null) { refspecs = Collections.singletonList(new RefSpec("+refs/heads/*:refs/remotes/" + origin + "/*")); } + if (filterSpec != null) { + launchCommand("config", "--add", "remote." + origin + ".promisor", "true"); + launchCommand("config", "--add", "remote." + origin + ".partialclonefilter", filterSpec); + } fetch_().from(urIish, refspecs) .shallow(shallow) .depth(depth) + .filter(filterSpec) .timeout(timeout) .tags(tags) .execute(); diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java b/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java index 92a23bbbf6..f3414417b9 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java @@ -114,4 +114,12 @@ public interface CloneCommand extends GitCommand { * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object. */ CloneCommand depth(Integer depth); + + /** + * Apply an object filter to a partial clone. If unset, a full clone is performed. + * + * @param filterSpec filter of objects to be sent by the server + * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object. + */ + CloneCommand filter(String filterSpec); } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/FetchCommand.java b/src/main/java/org/jenkinsci/plugins/gitclient/FetchCommand.java index 01320584f1..20ad3df564 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/FetchCommand.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/FetchCommand.java @@ -64,4 +64,12 @@ public interface FetchCommand extends GitCommand { * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object. */ FetchCommand depth(Integer depth); + + /** + * Apply an object filter to a partial clone. If unset, a full clone is performed. + * + * @param filterSpec filter of objects to be sent by the server + * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object. + */ + FetchCommand filter(String filterSpec); } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index 94e63ed330..ace5901a4c 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -786,6 +786,12 @@ public org.jenkinsci.plugins.gitclient.FetchCommand depth(Integer depth) { return this; } + @Override + public org.jenkinsci.plugins.gitclient.FetchCommand filter(String filterSpec) { + // unsupported, revert to full clone for backward compatibility + return this; + } + @Override public void execute() throws GitException { try (Repository repo = getRepository()) { @@ -1638,6 +1644,12 @@ public CloneCommand depth(Integer depth) { return this; } + @Override + public CloneCommand filter(String filterSpec) { + // unsupported, revert to full clone for backward compatibility + return this; + } + private RepositoryBuilder newRepositoryBuilder() { RepositoryBuilder builder = new RepositoryBuilder(); builder.setGitDir(new File(workspace, Constants.DOT_GIT)).readEnvironment(); From ee80d7cf9f27366869d45ffd752f80d8bdd8e347 Mon Sep 17 00:00:00 2001 From: Thomas Duboucher Date: Sun, 13 Jul 2025 22:59:46 +0200 Subject: [PATCH 02/10] Add authentication during checkout with partial clone --- .../plugins/gitclient/CliGitAPIImpl.java | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index a156786257..62d92eece4 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -1786,6 +1786,15 @@ public boolean isShallowRepository() { return new File(workspace, pathJoin(".git", "shallow")).exists(); } + /** Returns true if the remote has a promisor configured for missing blobs. */ + public boolean hasPromisor(String name) throws GitException, InterruptedException { + try { + return launchCommand("config", "remote." + name + ".promisor").contains("true"); + } catch (GitException ge) { + return false; + } + } + private String pathJoin(String a, String b) { return new File(a, b).toString(); } @@ -2115,6 +2124,17 @@ private String launchCommandWithCredentials( @NonNull URIish url, Integer timeout) throws GitException, InterruptedException { + return launchCommandWithCredentials(args, workDir, environment, credentials, url, timeout); + } + + private String launchCommandWithCredentials( + ArgumentListBuilder args, + File workDir, + EnvVars env, + StandardCredentials credentials, + @NonNull URIish url, + Integer timeout) + throws GitException, InterruptedException { Path key = null; Path ssh = null; @@ -2123,7 +2143,6 @@ private String launchCommandWithCredentials( Path passwordFile = null; Path passphrase = null; Path knownHostsTemp = null; - EnvVars env = environment; if (!PROMPT_FOR_AUTHENTICATION && isAtLeastVersion(2, 3, 0, 0)) { env = new EnvVars(env); env.put("GIT_TERMINAL_PROMPT", "false"); // Don't prompt for auth from command line git @@ -3204,7 +3223,32 @@ public void execute() throws GitException, InterruptedException { args.add("-f"); } args.add(ref); - launchCommandIn(args, workspace, checkoutEnv, timeout); + + String repoUrl = null; + try { + String defaultRemote = getDefaultRemote(); + if (defaultRemote != null && !defaultRemote.isEmpty() && hasPromisor(defaultRemote)) { + repoUrl = getRemoteUrl(defaultRemote); + } + } catch (GitException e) { + /* Nothing to do, just keeping repoUrl = null */ + } + + if (repoUrl != null) { + StandardCredentials cred = credentials.get(repoUrl); + if (cred == null) { + cred = defaultCredentials; + } + + try { + launchCommandWithCredentials( + args, workspace, checkoutEnv, cred, new URIish(repoUrl), timeout); + } catch (URISyntaxException e) { + throw new GitException("Invalid URL " + repoUrl, e); + } + } else { + launchCommandIn(args, workspace, checkoutEnv, timeout); + } if (lfsRemote != null) { final String url = getRemoteUrl(lfsRemote); From 1b653a610abdd5bdbd2ebc8d5383923de63a18b2 Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Tue, 22 Jul 2025 16:01:49 +0200 Subject: [PATCH 03/10] Support changing the filter specification in an existings repository --- .../plugins/gitclient/CliGitAPIImpl.java | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 62d92eece4..e5099a4131 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -599,10 +599,36 @@ public void execute() throws GitException, InterruptedException { args.add("--depth=" + depth); } - if (filterSpec != null && isAtLeastVersion(2, 22, 0, 0)) { - // in the future, we could add --refetch if we detect a change in the filter configuration to - // trigger maintenance - args.add("--filter=" + filterSpec); + if (filterSpec != null) { + // try to get the current filterspec + String currentFilterSpec = null; + String defaultRemote = null; + try { + defaultRemote = getDefaultRemote(); + if (defaultRemote != null && !defaultRemote.isEmpty()) { + currentFilterSpec = launchCommand( + "config", "remote." + defaultRemote + ".partialclonefilter"); + } + // We might fail if we have no modules, so catch this + // exception and just return. + } catch (GitException e) { + // leave currentFilterSpec null + } + + if (isAtLeastVersion(2, 22, 0, 0)) { + args.add("--filter=" + filterSpec); + } + + // if the filterspec has changed + if (defaultRemote != null && !filterSpec.equals(currentFilterSpec)) { + launchCommand("config", "--add", "remote." + defaultRemote + ".promisor", "true"); + launchCommand("config", "--add", "remote." + defaultRemote + ".partialclonefilter", filterSpec); + + if (isAtLeastVersion(2, 36, 0, 0)) { + // reapply filter and trigger maintenance + args.add("--refetch"); + } + } } warnIfWindowsTemporaryDirNameHasSpaces(); From 1a4ef6df64ba40752b6d69ebaa9e9f96db4b54ba Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Tue, 22 Jul 2025 16:02:20 +0200 Subject: [PATCH 04/10] Add tests for clone and fetch with filter --- .../plugins/gitclient/GitClientCloneTest.java | 35 +++++++++++++++++++ .../plugins/gitclient/GitClientFetchTest.java | 19 ++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java index b1a25c23ae..5dc9f82639 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java @@ -17,6 +17,8 @@ import hudson.remoting.VirtualChannel; import hudson.util.StreamTaskListener; import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; @@ -427,12 +429,45 @@ void test_clone_huge_timeout_logging() throws Exception { assertTimeout(testGitClient, "fetch", expectedValue); } + @Test + @Issue("JENKINS-70094") + public void test_clone_filter() throws Exception { + CloneCommand cmd = testGitClient + .clone_() + .url(workspace.localMirror()) + .repositoryName("origin") + .filter("blob:none"); + cmd.execute(); + testGitClient.checkout().ref("origin/master").branch("master").execute(); + check_remote_url(workspace, testGitClient, "origin"); + assertBranchesExist(testGitClient.getBranches(), "master"); + assertAlternatesFileNotFound(); + boolean expectedPromisorValue = gitImplName.equals("git"); + assertThat("hasPromisor?", workspace.cgit().hasPromisor("origin"), is(expectedPromisorValue)); + if (gitImplName.equals("git")) { + String filterSpec = workspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); + assertThat("filterSpec", filterSpec, is("blob:none")); + //assertPromisorFilesExist(); + } + } + private void assertAlternatesFileExists() { final String alternates = ".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates"; assertThat(new File(testGitDir, alternates), is(anExistingFile())); } + private void assertPromisorFilesExist() { + File pack = new File(".git" + File.separator + "objects" + File.separator + "pack"); + File[] promisors = pack.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".promisor"); + } + }); + assertThat(promisors, is(not(emptyArray()))); + } + private void assertAlternatesFileNotFound() { final String alternates = ".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates"; diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java index f2764ff8ea..cffbd8a8b7 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java @@ -620,6 +620,25 @@ void test_fetch_default_timeout_logging() throws Exception { assertRevParseNotCalled(testGitClient, randomBranchName); } + @Test + @Issue("JENKINS-70094") + public void test_fetch_filter() throws Exception { + testGitClient + .clone_() + .url(workspace.localMirror()) + .repositoryName("origin") + .filter("blob:none") + .execute(); + checkoutRandomBranch(); + testGitClient.fetch_().from(new URIish("origin"), null).filter("blob:limit=1k").execute(); + boolean expectedPromisorValue = gitImplName.equals("git"); + assertThat("hasPromisor?", workspace.cgit().hasPromisor("origin"), is(expectedPromisorValue)); + if (gitImplName.equals("git")) { + String filterSpec = workspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); + assertThat("filterSpec", filterSpec, is("blob:limit=1k")); + } + } + private void check_remote_url(WorkspaceWithRepo workspace, GitClient gitClient, final String repositoryName) throws Exception { assertThat("Wrong remote URL", gitClient.getRemoteUrl(repositoryName), is(workspace.localMirror())); From 527a61e37a27b1b0d6c0020d0b2aa865142f2938 Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Thu, 31 Jul 2025 10:26:36 +0200 Subject: [PATCH 05/10] Fix filterspec being appended instead of overwritten when filterspec has changed --- .../java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index e5099a4131..dcbd1ef8fd 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -621,8 +621,8 @@ public void execute() throws GitException, InterruptedException { // if the filterspec has changed if (defaultRemote != null && !filterSpec.equals(currentFilterSpec)) { - launchCommand("config", "--add", "remote." + defaultRemote + ".promisor", "true"); - launchCommand("config", "--add", "remote." + defaultRemote + ".partialclonefilter", filterSpec); + launchCommand("config", "remote." + defaultRemote + ".promisor", "true"); + launchCommand("config", "remote." + defaultRemote + ".partialclonefilter", filterSpec); if (isAtLeastVersion(2, 36, 0, 0)) { // reapply filter and trigger maintenance From 673c5a975d11b07d7ae9db4217bfc31dd01d8897 Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Thu, 31 Jul 2025 10:59:27 +0200 Subject: [PATCH 06/10] Use empty workspace for partial clone tests --- .../plugins/gitclient/GitClientCloneTest.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java index 5dc9f82639..dadd36ed50 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java @@ -432,22 +432,23 @@ void test_clone_huge_timeout_logging() throws Exception { @Test @Issue("JENKINS-70094") public void test_clone_filter() throws Exception { - CloneCommand cmd = testGitClient + WorkspaceWithRepo anotherWorkspace = new WorkspaceWithRepo(secondRepo.getRoot(), "git", listener); + GitClient anotherGitClient = anotherWorkspace.getGitClient(); + CloneCommand cmd = anotherGitClient .clone_() - .url(workspace.localMirror()) + .url(anotherWorkspace.localMirror()) .repositoryName("origin") .filter("blob:none"); cmd.execute(); - testGitClient.checkout().ref("origin/master").branch("master").execute(); - check_remote_url(workspace, testGitClient, "origin"); - assertBranchesExist(testGitClient.getBranches(), "master"); + anotherGitClient.checkout().ref("origin/master").branch("master").execute(); + check_remote_url(anotherWorkspace, anotherGitClient, "origin"); + assertBranchesExist(anotherGitClient.getBranches(), "master"); assertAlternatesFileNotFound(); - boolean expectedPromisorValue = gitImplName.equals("git"); - assertThat("hasPromisor?", workspace.cgit().hasPromisor("origin"), is(expectedPromisorValue)); if (gitImplName.equals("git")) { - String filterSpec = workspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); + assertThat("hasPromisor?", anotherWorkspace.cgit().hasPromisor("origin"), is(true)); + String filterSpec = anotherWorkspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); assertThat("filterSpec", filterSpec, is("blob:none")); - //assertPromisorFilesExist(); + assertPromisorFilesExist(anotherWorkspace.getGitFileDir()); } } @@ -457,8 +458,8 @@ private void assertAlternatesFileExists() { assertThat(new File(testGitDir, alternates), is(anExistingFile())); } - private void assertPromisorFilesExist() { - File pack = new File(".git" + File.separator + "objects" + File.separator + "pack"); + private void assertPromisorFilesExist(File anotherTestGitDir) { + File pack = new File(anotherTestGitDir, ".git" + File.separator + "objects" + File.separator + "pack"); File[] promisors = pack.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { From ad97fe3ec664883b1aa9ca14008a1aa2e5780307 Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Thu, 31 Jul 2025 10:59:41 +0200 Subject: [PATCH 07/10] Add test for filter refetch --- .../plugins/gitclient/GitClientFetchTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java index cffbd8a8b7..8603274f87 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java @@ -639,6 +639,30 @@ public void test_fetch_filter() throws Exception { } } + @Test + @Issue("JENKINS-70094") + public void test_fetch_filter_changed() throws Exception { + testGitClient + .clone_() + .url(workspace.localMirror()) + .repositoryName("origin") + .filter("blob:none") + .execute(); + checkoutRandomBranch(); + testGitClient.fetch_().from(new URIish("origin"), null).filter("tree:0").execute(); + boolean expectedPromisorValue = gitImplName.equals("git"); + assertThat("hasPromisor?", workspace.cgit().hasPromisor("origin"), is(expectedPromisorValue)); + if (gitImplName.equals("git")) { + String filterSpec = workspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); + assertThat("filterSpec", filterSpec, is("tree:0")); + } + testGitClient.fetch_().from(new URIish("origin"), null).filter("blob:limit=1k").execute(); + if (gitImplName.equals("git")) { + String filterSpec = workspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); + assertThat("filterSpec", filterSpec, is("blob:limit=1k")); + } + } + private void check_remote_url(WorkspaceWithRepo workspace, GitClient gitClient, final String repositoryName) throws Exception { assertThat("Wrong remote URL", gitClient.getRemoteUrl(repositoryName), is(workspace.localMirror())); From 33eb4a92649277102b8f099f63c6e3694977b570 Mon Sep 17 00:00:00 2001 From: Thomas Duboucher Date: Tue, 2 Jun 2026 17:09:31 +0200 Subject: [PATCH 08/10] (chore) Apply spotless --- .../plugins/gitclient/CliGitAPIImpl.java | 4 +-- .../plugins/gitclient/GitClientCloneTest.java | 6 ++-- .../plugins/gitclient/GitClientFetchTest.java | 28 ++++++++++++++----- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index dcbd1ef8fd..2e85f59afa 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -606,8 +606,8 @@ public void execute() throws GitException, InterruptedException { try { defaultRemote = getDefaultRemote(); if (defaultRemote != null && !defaultRemote.isEmpty()) { - currentFilterSpec = launchCommand( - "config", "remote." + defaultRemote + ".partialclonefilter"); + currentFilterSpec = + launchCommand("config", "remote." + defaultRemote + ".partialclonefilter"); } // We might fail if we have no modules, so catch this // exception and just return. diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java index dadd36ed50..850ac9e9c9 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java @@ -18,7 +18,6 @@ import hudson.util.StreamTaskListener; import java.io.File; import java.io.FilenameFilter; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; @@ -42,6 +41,7 @@ import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.jvnet.hudson.test.Issue; @ParameterizedClass(name = "{0}") @MethodSource("gitObjects") @@ -446,7 +446,9 @@ public void test_clone_filter() throws Exception { assertAlternatesFileNotFound(); if (gitImplName.equals("git")) { assertThat("hasPromisor?", anotherWorkspace.cgit().hasPromisor("origin"), is(true)); - String filterSpec = anotherWorkspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); + String filterSpec = anotherWorkspace + .launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter") + .trim(); assertThat("filterSpec", filterSpec, is("blob:none")); assertPromisorFilesExist(anotherWorkspace.getGitFileDir()); } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java index 8603274f87..3c0cdfb112 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java @@ -630,11 +630,17 @@ public void test_fetch_filter() throws Exception { .filter("blob:none") .execute(); checkoutRandomBranch(); - testGitClient.fetch_().from(new URIish("origin"), null).filter("blob:limit=1k").execute(); + testGitClient + .fetch_() + .from(new URIish("origin"), null) + .filter("blob:limit=1k") + .execute(); boolean expectedPromisorValue = gitImplName.equals("git"); assertThat("hasPromisor?", workspace.cgit().hasPromisor("origin"), is(expectedPromisorValue)); if (gitImplName.equals("git")) { - String filterSpec = workspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); + String filterSpec = workspace + .launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter") + .trim(); assertThat("filterSpec", filterSpec, is("blob:limit=1k")); } } @@ -653,15 +659,23 @@ public void test_fetch_filter_changed() throws Exception { boolean expectedPromisorValue = gitImplName.equals("git"); assertThat("hasPromisor?", workspace.cgit().hasPromisor("origin"), is(expectedPromisorValue)); if (gitImplName.equals("git")) { - String filterSpec = workspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); + String filterSpec = workspace + .launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter") + .trim(); assertThat("filterSpec", filterSpec, is("tree:0")); } - testGitClient.fetch_().from(new URIish("origin"), null).filter("blob:limit=1k").execute(); + testGitClient + .fetch_() + .from(new URIish("origin"), null) + .filter("blob:limit=1k") + .execute(); if (gitImplName.equals("git")) { - String filterSpec = workspace.launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter").trim(); - assertThat("filterSpec", filterSpec, is("blob:limit=1k")); - } + String filterSpec = workspace + .launchCommand("git", "config", "remote." + "origin" + ".partialclonefilter") + .trim(); + assertThat("filterSpec", filterSpec, is("blob:limit=1k")); } + } private void check_remote_url(WorkspaceWithRepo workspace, GitClient gitClient, final String repositoryName) throws Exception { From 00f771197ea95a48642dfd787b744d04ac911f24 Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Mon, 8 Jun 2026 11:55:40 +0200 Subject: [PATCH 09/10] Code review --- .../jenkinsci/plugins/gitclient/CliGitAPIImpl.java | 5 ++--- .../org/jenkinsci/plugins/gitclient/CloneCommand.java | 1 + .../org/jenkinsci/plugins/gitclient/FetchCommand.java | 1 + .../org/jenkinsci/plugins/gitclient/JGitAPIImpl.java | 10 ++++++++-- .../plugins/gitclient/GitClientCloneTest.java | 11 ++--------- .../plugins/gitclient/GitClientFetchTest.java | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 2e85f59afa..91b55790a9 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -609,8 +609,7 @@ public void execute() throws GitException, InterruptedException { currentFilterSpec = launchCommand("config", "remote." + defaultRemote + ".partialclonefilter"); } - // We might fail if we have no modules, so catch this - // exception and just return. + // we might fail if we have no promisor configured, catch the exception and just continue } catch (GitException e) { // leave currentFilterSpec null } @@ -1813,7 +1812,7 @@ public boolean isShallowRepository() { } /** Returns true if the remote has a promisor configured for missing blobs. */ - public boolean hasPromisor(String name) throws GitException, InterruptedException { + boolean hasPromisor(String name) throws GitException, InterruptedException { try { return launchCommand("config", "remote." + name + ".promisor").contains("true"); } catch (GitException ge) { diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java b/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java index f3414417b9..e085331d6b 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CloneCommand.java @@ -120,6 +120,7 @@ public interface CloneCommand extends GitCommand { * * @param filterSpec filter of objects to be sent by the server * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object. + * @since 6.7.0 */ CloneCommand filter(String filterSpec); } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/FetchCommand.java b/src/main/java/org/jenkinsci/plugins/gitclient/FetchCommand.java index 20ad3df564..ff8884eb2c 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/FetchCommand.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/FetchCommand.java @@ -70,6 +70,7 @@ public interface FetchCommand extends GitCommand { * * @param filterSpec filter of objects to be sent by the server * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object. + * @since 6.7.0 */ FetchCommand filter(String filterSpec); } diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java index ace5901a4c..4fbcc126ee 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/JGitAPIImpl.java @@ -788,7 +788,10 @@ public org.jenkinsci.plugins.gitclient.FetchCommand depth(Integer depth) { @Override public org.jenkinsci.plugins.gitclient.FetchCommand filter(String filterSpec) { - // unsupported, revert to full clone for backward compatibility + if (filterSpec != null) { + listener.getLogger() + .println("[WARNING] JGit does not support partial clone filters; reverting to full clone"); + } return this; } @@ -1646,7 +1649,10 @@ public CloneCommand depth(Integer depth) { @Override public CloneCommand filter(String filterSpec) { - // unsupported, revert to full clone for backward compatibility + if (filterSpec != null) { + listener.getLogger() + .println("[WARNING] JGit does not support partial clone filters; reverting to full clone"); + } return this; } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java index 850ac9e9c9..1daef7b7e3 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientCloneTest.java @@ -17,7 +17,6 @@ import hudson.remoting.VirtualChannel; import hudson.util.StreamTaskListener; import java.io.File; -import java.io.FilenameFilter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; @@ -431,7 +430,7 @@ void test_clone_huge_timeout_logging() throws Exception { @Test @Issue("JENKINS-70094") - public void test_clone_filter() throws Exception { + void test_clone_filter() throws Exception { WorkspaceWithRepo anotherWorkspace = new WorkspaceWithRepo(secondRepo.getRoot(), "git", listener); GitClient anotherGitClient = anotherWorkspace.getGitClient(); CloneCommand cmd = anotherGitClient @@ -443,7 +442,6 @@ public void test_clone_filter() throws Exception { anotherGitClient.checkout().ref("origin/master").branch("master").execute(); check_remote_url(anotherWorkspace, anotherGitClient, "origin"); assertBranchesExist(anotherGitClient.getBranches(), "master"); - assertAlternatesFileNotFound(); if (gitImplName.equals("git")) { assertThat("hasPromisor?", anotherWorkspace.cgit().hasPromisor("origin"), is(true)); String filterSpec = anotherWorkspace @@ -462,12 +460,7 @@ private void assertAlternatesFileExists() { private void assertPromisorFilesExist(File anotherTestGitDir) { File pack = new File(anotherTestGitDir, ".git" + File.separator + "objects" + File.separator + "pack"); - File[] promisors = pack.listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.endsWith(".promisor"); - } - }); + File[] promisors = pack.listFiles((dir, name) -> name.endsWith(".promisor")); assertThat(promisors, is(not(emptyArray()))); } diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java index 3c0cdfb112..70416e3866 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/GitClientFetchTest.java @@ -622,7 +622,7 @@ void test_fetch_default_timeout_logging() throws Exception { @Test @Issue("JENKINS-70094") - public void test_fetch_filter() throws Exception { + void test_fetch_filter() throws Exception { testGitClient .clone_() .url(workspace.localMirror()) @@ -647,7 +647,7 @@ public void test_fetch_filter() throws Exception { @Test @Issue("JENKINS-70094") - public void test_fetch_filter_changed() throws Exception { + void test_fetch_filter_changed() throws Exception { testGitClient .clone_() .url(workspace.localMirror()) From 57e4cd2d9276f368258af23649fe5b859b3ee763 Mon Sep 17 00:00:00 2001 From: Duboucher Thomas Date: Mon, 8 Jun 2026 15:26:46 +0200 Subject: [PATCH 10/10] Add basic credential test for partial clones --- .../plugins/gitclient/CredentialsTest.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java index 07b65878ad..25827d11e3 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CredentialsTest.java @@ -516,6 +516,38 @@ void testLfsMergeWithCredentials() throws Exception { "Fast-forward merge failed. master and modified_lfs should be the same."); } + @Test + @Issue("JENKINS-70094") + void testCheckoutPartialCloneWithCredentials() throws Exception { + if (testPeriodExpired() || lfsSpecificTest) { + return; + } + File clonedFile = new File(repo, fileToCheck); + git.init_().workspace(repo.getAbsolutePath()).execute(); + assertFalse(clonedFile.exists(), "file " + fileToCheck + " in " + repo + ", has " + listDir(repo)); + addCredential(); + // clone with remote url + git.clone_() + .url(gitRepoURL) + .repositoryName("origin") + .filter("blob:none") + .execute(); + ObjectId master = git.getHeadRev(gitRepoURL, "master"); + git.checkout() + .branch("master") + .ref(master.getName()) + .deleteBranchIfExist(true) + .execute(); + assertTrue(git.isCommitInRepo(master), "master: " + master + " not in repo"); + assertEquals( + master, + git.withRepository( + (gitRepo, unusedChannel) -> gitRepo.findRef("master").getObjectId()), + "Master != HEAD"); + assertEquals("master", git.withRepository((gitRepo, unusedChanel) -> gitRepo.getBranch()), "Wrong branch"); + assertTrue(clonedFile.exists(), "No file " + fileToCheck + ", has " + listDir(repo)); + } + private static boolean isWindows() { return File.pathSeparatorChar == ';'; }