From fdcdad42ff96fea3c05598e378d3c44ad4a51bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Thu, 1 Feb 2024 20:59:39 +0200 Subject: [PATCH] fix: do not try to load configuration from files that are not checked out --- .changeset/wet-cups-jog.md | 5 +++++ lib/searchConfigs.js | 23 ++++++++++++++--------- test/unit/searchConfigs.spec.js | 20 +++++++++++++++++++- 3 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 .changeset/wet-cups-jog.md diff --git a/.changeset/wet-cups-jog.md b/.changeset/wet-cups-jog.md new file mode 100644 index 000000000..2877ed111 --- /dev/null +++ b/.changeset/wet-cups-jog.md @@ -0,0 +1,5 @@ +--- +'lint-staged': patch +--- + +_Lint-staged_ no longer tries to load configuration from files that are not checked out. This might happen when using sparse-checkout. diff --git a/lib/searchConfigs.js b/lib/searchConfigs.js index ba878c01e..deee7ad10 100644 --- a/lib/searchConfigs.js +++ b/lib/searchConfigs.js @@ -13,10 +13,9 @@ import { CONFIG_FILE_NAMES } from './configFiles.js' const debugLog = debug('lint-staged:searchConfigs') -const EXEC_GIT = ['ls-files', '-z', '--full-name'] +const EXEC_GIT = ['ls-files', '-z', '--full-name', '-t'] -const filterPossibleConfigFiles = (files) => - files.filter((file) => CONFIG_FILE_NAMES.includes(path.basename(file))) +const filterPossibleConfigFiles = (file) => CONFIG_FILE_NAMES.includes(path.basename(file)) const numberOfLevels = (file) => file.split('/').length @@ -58,17 +57,23 @@ export const searchConfigs = async ( return { [configPath]: validateConfig(config, filepath, logger) } } - const [cachedFiles, otherFiles] = await Promise.all([ + const [cachedFilesWithStatus, otherFilesWithStatus] = await Promise.all([ /** Get all possible config files known to git */ - execGit(EXEC_GIT, { cwd: gitDir }).then(parseGitZOutput).then(filterPossibleConfigFiles), + execGit(EXEC_GIT, { cwd: gitDir }).then(parseGitZOutput), /** Get all possible config files from uncommitted files */ - execGit([...EXEC_GIT, '--others', '--exclude-standard'], { cwd: gitDir }) - .then(parseGitZOutput) - .then(filterPossibleConfigFiles), + execGit([...EXEC_GIT, '--others', '--exclude-standard'], { cwd: gitDir }).then(parseGitZOutput), ]) /** Sort possible config files so that deepest is first */ - const possibleConfigFiles = [...cachedFiles, ...otherFiles] + const possibleConfigFiles = [...cachedFilesWithStatus, ...otherFilesWithStatus] + .flatMap( + /** + * Leave out lines starting with "S " to ignore not-checked-out files in a sparse repo. + * The "S" status means a tracked file that is "skip-worktree" + * @see https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--t + */ (line) => (line.startsWith('S ') ? [] : [line.replace(/^[HSMRCK?U] /, '')]) + ) + .filter(filterPossibleConfigFiles) .map((file) => normalizePath(path.join(gitDir, file))) .filter(isInsideDirectory(cwd)) .sort(sortDeepestParth) diff --git a/test/unit/searchConfigs.spec.js b/test/unit/searchConfigs.spec.js index fcc156831..5a6d7eaa1 100644 --- a/test/unit/searchConfigs.spec.js +++ b/test/unit/searchConfigs.spec.js @@ -101,7 +101,7 @@ describe('searchConfigs', () => { const config = { '*.js': 'eslint' } execGit.mockResolvedValueOnce( - `.lintstagedrc.json\u0000even/deeper/.lintstagedrc.json\u0000deeper/.lintstagedrc.json\u0000` + `H .lintstagedrc.json\u0000H even/deeper/.lintstagedrc.json\u0000H deeper/.lintstagedrc.json\u0000` ) const topLevelConfig = normalizePath(path.join(process.cwd(), '.lintstagedrc.json')) @@ -118,4 +118,22 @@ describe('searchConfigs', () => { expect(Object.keys(configs)).toEqual([evenDeeperConfig, deeperConfig, topLevelConfig]) }) + + it('should ignore config files skipped from the worktree (sparse checkout)', async () => { + const config = { '*.js': 'eslint' } + + execGit.mockResolvedValueOnce(`H .lintstagedrc.json\u0000S skipped/.lintstagedrc.json\u0000`) + + const topLevelConfig = normalizePath(path.join(process.cwd(), '.lintstagedrc.json')) + const skippedConfig = normalizePath(path.join(process.cwd(), 'skipped/.lintstagedrc.json')) + + loadConfig.mockResolvedValueOnce({ config, filepath: topLevelConfig }) + + // Mock will return config for skipped file, but it should not be read + loadConfig.mockResolvedValueOnce({ config, filepath: skippedConfig }) + + const configs = await searchConfigs({}) + + expect(Object.keys(configs)).toEqual([topLevelConfig]) + }) })