From 4fba4c42afb21f2bb198a9da39b42fd97dc0d3e9 Mon Sep 17 00:00:00 2001 From: Jellyfrog Date: Fri, 2 Oct 2026 01:45:20 +0200 Subject: [PATCH] Allow filter: none to disable the implicit blob:none for sparse checkout When sparse-checkout is set and filter is not, checkout fetches with --filter=blob:none. If the sparse patterns keep most of the repository, the following checkout fetches each blob lazily from the promisor remote, which is slower than one full pack. Setting filter to 'none' now fetches without a filter. 'none' is not a valid git filter-spec, so no existing workflow can depend on it. The effective filter is now resolved in input-helper, so git-source-provider has one decision point. --- README.md | 3 +- __test__/git-command-manager.test.ts | 35 ++++++++++++++++++++++++ __test__/input-helper.test.ts | 41 ++++++++++++++++++++++++++++ action.yml | 3 +- dist/index.js | 23 ++++++++++------ src/git-source-provider.ts | 2 -- src/git-source-settings.ts | 3 +- src/input-helper.ts | 22 +++++++++------ 8 files changed, 110 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5509e7d1..59acda12 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,8 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/ # Default: true clean: '' - # Partially clone against a given filter. Overrides sparse-checkout if set. + # Partially clone against a given filter. If sparse-checkout is set and filter is + # not, the filter is `blob:none`. Set to `none` to fetch without a filter. # Default: null filter: '' diff --git a/__test__/git-command-manager.test.ts b/__test__/git-command-manager.test.ts index e669c0f3..f03b587e 100644 --- a/__test__/git-command-manager.test.ts +++ b/__test__/git-command-manager.test.ts @@ -401,6 +401,41 @@ describe('Test fetchDepth and fetchTags options', () => { expect.any(Object) ) }) + + it('should not pass --filter when filter is not set', async () => { + // exec.exec is already mockExec + const workingDirectory = 'test' + const lfs = false + const doSparseCheckout = false + git = await commandManager.createCommandManager( + workingDirectory, + lfs, + doSparseCheckout + ) + + const refSpec = ['refspec1'] + const options = { + fetchDepth: 1 + } + + await git.fetch(refSpec, options) + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + [ + '-c', + 'protocol.version=2', + 'fetch', + '--no-tags', + '--prune', + '--no-recurse-submodules', + '--depth=1', + 'origin', + 'refspec1' + ], + expect.any(Object) + ) + }) }) describe('repository initialization object format', () => { diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts index afe30fb8..323c9ed9 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -215,6 +215,47 @@ describe('input-helper tests', () => { expect(settings.commit).toBe('1111111111222222222233333333334444444444') }) + describe('filter', () => { + it('is not set without sparse checkout', async () => { + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.filter).toBe(undefined) + }) + + it('defaults to blob:none with sparse checkout', async () => { + inputs['sparse-checkout'] = 'src\ndocs' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.sparseCheckout).toEqual(['src', 'docs']) + expect(settings.filter).toBe('blob:none') + }) + + it('uses the given filter with sparse checkout', async () => { + inputs['sparse-checkout'] = 'src' + inputs.filter = 'tree:0' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.filter).toBe('tree:0') + }) + + it('uses the given filter without sparse checkout', async () => { + inputs.filter = 'blob:limit=1m' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.filter).toBe('blob:limit=1m') + }) + + it('is not set when none with sparse checkout', async () => { + inputs['sparse-checkout'] = 'src' + inputs.filter = 'none' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.sparseCheckout).toEqual(['src']) + expect(settings.filter).toBe(undefined) + }) + + it('is not set when none without sparse checkout', async () => { + inputs.filter = 'None' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.filter).toBe(undefined) + }) + }) + it('sets workflow organization ID', async () => { const settings: IGitSourceSettings = await inputHelper.getInputs() expect(settings.workflowOrganizationId).toBe(123456) diff --git a/action.yml b/action.yml index 5b0524f7..78875c36 100644 --- a/action.yml +++ b/action.yml @@ -60,7 +60,8 @@ inputs: filter: description: > Partially clone against a given filter. - Overrides sparse-checkout if set. + If sparse-checkout is set and filter is not, the filter is `blob:none`. + Set to `none` to fetch without a filter. default: null sparse-checkout: description: > diff --git a/dist/index.js b/dist/index.js index 06ae5d22..8caa5751 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41810,9 +41810,6 @@ async function getSource(settings) { if (settings.filter) { fetchOptions.filter = settings.filter; } - else if (settings.sparseCheckout) { - fetchOptions.filter = 'blob:none'; - } if (settings.fetchDepth <= 0) { // Fetch all branches and tags let refSpec = getRefSpecForAllHistory(settings.ref, settings.commit); @@ -42137,18 +42134,26 @@ async function getInputs() { // Clean result.clean = (getInput('clean') || 'true').toUpperCase() === 'TRUE'; core_debug(`clean = ${result.clean}`); - // Filter - const filter = getInput('filter'); - if (filter) { - result.filter = filter; - } - core_debug(`filter = ${result.filter}`); // Sparse checkout const sparseCheckout = getMultilineInput('sparse-checkout'); if (sparseCheckout.length) { result.sparseCheckout = sparseCheckout; core_debug(`sparse checkout = ${result.sparseCheckout}`); } + // Filter + // Sparse checkout implies 'blob:none' unless a filter is given. + // 'none' disables the filter. + const filter = getInput('filter'); + if (filter.toUpperCase() === 'NONE') { + result.filter = undefined; + } + else if (filter) { + result.filter = filter; + } + else if (result.sparseCheckout) { + result.filter = 'blob:none'; + } + core_debug(`filter = ${result.filter}`); result.sparseCheckoutConeMode = (getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() === 'TRUE'; diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index b9c1d357..dad5d358 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -181,8 +181,6 @@ export async function getSource(settings: IGitSourceSettings): Promise { if (settings.filter) { fetchOptions.filter = settings.filter - } else if (settings.sparseCheckout) { - fetchOptions.filter = 'blob:none' } if (settings.fetchDepth <= 0) { diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts index 79041c43..eab36688 100644 --- a/src/git-source-settings.ts +++ b/src/git-source-settings.ts @@ -30,7 +30,8 @@ export interface IGitSourceSettings { clean: boolean /** - * The filter determining which objects to include + * The filter determining which objects to include. + * Defaults to 'blob:none' when sparse checkout is set, unless the input is 'none'. */ filter: string | undefined diff --git a/src/input-helper.ts b/src/input-helper.ts index 9a98b867..b57150de 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -100,14 +100,6 @@ export async function getInputs(): Promise { result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE' core.debug(`clean = ${result.clean}`) - // Filter - const filter = core.getInput('filter') - if (filter) { - result.filter = filter - } - - core.debug(`filter = ${result.filter}`) - // Sparse checkout const sparseCheckout = core.getMultilineInput('sparse-checkout') if (sparseCheckout.length) { @@ -115,6 +107,20 @@ export async function getInputs(): Promise { core.debug(`sparse checkout = ${result.sparseCheckout}`) } + // Filter + // Sparse checkout implies 'blob:none' unless a filter is given. + // 'none' disables the filter. + const filter = core.getInput('filter') + if (filter.toUpperCase() === 'NONE') { + result.filter = undefined + } else if (filter) { + result.filter = filter + } else if (result.sparseCheckout) { + result.filter = 'blob:none' + } + + core.debug(`filter = ${result.filter}`) + result.sparseCheckoutConeMode = (core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() === 'TRUE'