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.
This commit is contained in:
Jellyfrog 2026-10-02 01:45:20 +02:00
parent f548e57e54
commit 4fba4c42af
8 changed files with 110 additions and 22 deletions

View File

@ -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: ''

View File

@ -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', () => {

View File

@ -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)

View File

@ -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: >

23
dist/index.js vendored
View File

@ -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';

View File

@ -181,8 +181,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
if (settings.filter) {
fetchOptions.filter = settings.filter
} else if (settings.sparseCheckout) {
fetchOptions.filter = 'blob:none'
}
if (settings.fetchDepth <= 0) {

View File

@ -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

View File

@ -100,14 +100,6 @@ export async function getInputs(): Promise<IGitSourceSettings> {
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<IGitSourceSettings> {
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'