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 5509e7d..59acda1 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 e669c0f..f03b587 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 afe30fb..323c9ed 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 5b0524f..78875c3 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 06ae5d2..8caa575 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 b9c1d35..dad5d35 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 79041c4..eab3668 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 9a98b86..b57150d 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'