Skip to content

Commit d8fdf1d

Browse files
committed
fix: limit configuration discovery to cwd
1 parent 1b1f0e4 commit d8fdf1d

11 files changed

+343
-259
lines changed

Diff for: lib/getConfigGroups.js

-105
This file was deleted.

Diff for: lib/groupFilesByConfig.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import path from 'path'
2+
3+
import debug from 'debug'
4+
5+
import { ConfigObjectSymbol } from './searchConfigs.js'
6+
7+
const debugLog = debug('lint-staged:groupFilesByConfig')
8+
9+
export const groupFilesByConfig = async ({ configs, files }) => {
10+
debugLog('Grouping %d files by %d configurations', files.length, Object.keys(configs).length)
11+
12+
const filesSet = new Set(files)
13+
const filesByConfig = {}
14+
15+
/** Configs are sorted deepest first by `searchConfigs` */
16+
for (const filepath of Reflect.ownKeys(configs)) {
17+
const config = configs[filepath]
18+
19+
/** When passed an explicit config object via the Node.js API, skip logic */
20+
if (filepath === ConfigObjectSymbol) {
21+
filesByConfig[filepath] = { config, files }
22+
break
23+
}
24+
25+
const dir = path.normalize(path.dirname(filepath))
26+
27+
/** Check if file is inside directory of the configuration file */
28+
const isInsideDir = (file) => {
29+
const relative = path.relative(dir, file)
30+
return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
31+
}
32+
33+
const scopedFiles = new Set()
34+
35+
/**
36+
* If file is inside the config file's directory, assign it to that configuration
37+
* and remove it from the set. This means only one configuration can match a file.
38+
*/
39+
filesSet.forEach((file) => {
40+
if (isInsideDir(file)) {
41+
scopedFiles.add(file)
42+
}
43+
})
44+
45+
scopedFiles.forEach((file) => {
46+
filesSet.delete(file)
47+
})
48+
49+
filesByConfig[filepath] = { config, files: Array.from(scopedFiles) }
50+
}
51+
52+
return filesByConfig
53+
}

Diff for: lib/index.js

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import debug from 'debug'
22

3-
import { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } from './messages.js'
3+
import {
4+
PREVENTED_EMPTY_COMMIT,
5+
GIT_ERROR,
6+
RESTORE_STASH_EXAMPLE,
7+
NO_CONFIGURATION,
8+
} from './messages.js'
49
import { printTaskOutput } from './printTaskOutput.js'
510
import { runAll } from './runAll.js'
6-
import { ApplyEmptyCommitError, GetBackupStashError, GitError } from './symbols.js'
11+
import {
12+
ApplyEmptyCommitError,
13+
ConfigNotFoundError,
14+
GetBackupStashError,
15+
GitError,
16+
} from './symbols.js'
717
import { validateOptions } from './validateOptions.js'
818

919
const debugLog = debug('lint-staged')
@@ -78,7 +88,10 @@ const lintStaged = async (
7888
} catch (runAllError) {
7989
if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
8090
const { ctx } = runAllError
81-
if (ctx.errors.has(ApplyEmptyCommitError)) {
91+
92+
if (ctx.errors.has(ConfigNotFoundError)) {
93+
logger.error(NO_CONFIGURATION)
94+
} else if (ctx.errors.has(ApplyEmptyCommitError)) {
8295
logger.warn(PREVENTED_EMPTY_COMMIT)
8396
} else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
8497
logger.error(GIT_ERROR)

Diff for: lib/messages.js

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export const incorrectBraces = (before, after) =>
2222
`
2323
)
2424

25+
export const NO_CONFIGURATION = `${error} No valid configuration found.`
26+
2527
export const NO_STAGED_FILES = `${info} No staged files found.`
2628

2729
export const NO_TASKS = `${info} No staged files match any configured task.`

Diff for: lib/runAll.js

+15-12
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import normalize from 'normalize-path'
1010
import { chunkFiles } from './chunkFiles.js'
1111
import { execGit } from './execGit.js'
1212
import { generateTasks } from './generateTasks.js'
13-
import { getConfigGroups } from './getConfigGroups.js'
1413
import { getRenderer } from './getRenderer.js'
1514
import { getStagedFiles } from './getStagedFiles.js'
1615
import { GitWorkflow } from './gitWorkflow.js'
16+
import { groupFilesByConfig } from './groupFilesByConfig.js'
1717
import { makeCmdTasks } from './makeCmdTasks.js'
1818
import {
1919
DEPRECATED_GIT_ADD,
@@ -36,7 +36,7 @@ import {
3636
restoreUnstagedChangesSkipped,
3737
} from './state.js'
3838
import { GitRepoError, GetStagedFilesError, GitError, ConfigNotFoundError } from './symbols.js'
39-
import { searchConfigs } from './searchConfigs.js'
39+
import { ConfigObjectSymbol, searchConfigs } from './searchConfigs.js'
4040

4141
const debugLog = debug('lint-staged:runAll')
4242

@@ -120,19 +120,16 @@ export const runAll = async (
120120
return ctx
121121
}
122122

123-
const configGroups = await getConfigGroups({ configObject, configPath, cwd, files }, logger)
124-
125-
const hasExplicitConfig = configObject || configPath
126-
const foundConfigs = hasExplicitConfig ? null : await searchConfigs(gitDir, logger)
127-
const numberOfConfigs = hasExplicitConfig ? 1 : Object.keys(foundConfigs).length
123+
const foundConfigs = await searchConfigs({ configObject, configPath, cwd, gitDir }, logger)
124+
const numberOfConfigs = Reflect.ownKeys(foundConfigs).length
128125

129126
// Throw if no configurations were found
130127
if (numberOfConfigs === 0) {
131128
ctx.errors.add(ConfigNotFoundError)
132129
throw createError(ctx, ConfigNotFoundError)
133130
}
134131

135-
debugLog('Found %d configs:\n%O', numberOfConfigs, foundConfigs)
132+
const filesByConfig = await groupFilesByConfig({ configs: foundConfigs, files })
136133

137134
const hasMultipleConfigs = numberOfConfigs > 1
138135

@@ -152,8 +149,14 @@ export const runAll = async (
152149
// Set of all staged files that matched a task glob. Values in a set are unique.
153150
const matchedFiles = new Set()
154151

155-
for (const [configPath, { config, files }] of Object.entries(configGroups)) {
156-
const relativeConfig = normalize(path.relative(cwd, configPath))
152+
for (const configPath of Reflect.ownKeys(filesByConfig)) {
153+
const { config, files } = filesByConfig[configPath]
154+
155+
const configName =
156+
configPath === ConfigObjectSymbol
157+
? 'Config object'
158+
: normalize(path.relative(cwd, configPath))
159+
157160
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
158161

159162
// Use actual cwd if it's specified, or there's only a single config file.
@@ -219,15 +222,15 @@ export const runAll = async (
219222

220223
listrTasks.push({
221224
title:
222-
`${relativeConfig}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
225+
`${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
223226
(chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
224227
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
225228
skip: () => {
226229
// Skip if the first step (backup) failed
227230
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
228231
// Skip chunk when no every task is skipped (due to no matches)
229232
if (chunkListrTasks.every((task) => task.skip())) {
230-
return `${relativeConfig}${dim(' — no tasks to run')}`
233+
return `${configName}${dim(' — no tasks to run')}`
231234
}
232235
return false
233236
},

Diff for: lib/searchConfigs.js

+59-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import { basename, join } from 'path'
44

5+
import debug from 'debug'
56
import normalize from 'normalize-path'
67

78
import { execGit } from './execGit.js'
89
import { loadConfig, searchPlaces } from './loadConfig.js'
910
import { parseGitZOutput } from './parseGitZOutput.js'
1011
import { validateConfig } from './validateConfig.js'
1112

13+
const debugLog = debug('lint-staged:searchConfigs')
14+
1215
const EXEC_GIT = ['ls-files', '-z', '--full-name']
1316

1417
const filterPossibleConfigFiles = (file) => searchPlaces.includes(basename(file))
@@ -17,14 +20,44 @@ const numberOfLevels = (file) => file.split('/').length
1720

1821
const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
1922

23+
const isInsideDirectory = (dir) => (file) => file.startsWith(normalize(dir))
24+
25+
export const ConfigObjectSymbol = Symbol()
26+
2027
/**
21-
* Search all config files from the git repository
28+
* Search all config files from the git repository, preferring those inside `cwd`.
2229
*
23-
* @param {string} gitDir
30+
* @param {object} options
31+
* @param {Object} [options.configObject] - Explicit config object from the js API
32+
* @param {string} [options.configPath] - Explicit path to a config file
33+
* @param {string} [options.cwd] - Current working directory
2434
* @param {Logger} logger
25-
* @returns {Promise<{ [key: string]: * }>} found configs with filepath as key, and config as value
35+
*
36+
* @returns {Promise<{ [key: string]: { config: *, files: string[] } }>} found configs with filepath as key, and config as value
2637
*/
27-
export const searchConfigs = async (gitDir = process.cwd(), logger) => {
38+
export const searchConfigs = async (
39+
{ configObject, configPath, cwd = process.cwd(), gitDir = cwd },
40+
logger
41+
) => {
42+
debugLog('Searching for configuration files...')
43+
44+
// Return explicit config object from js API
45+
if (configObject) {
46+
debugLog('Using single direct configuration object...')
47+
48+
return { [ConfigObjectSymbol]: validateConfig(configObject, 'config object', logger) }
49+
}
50+
51+
// Use only explicit config path instead of discovering multiple
52+
if (configPath) {
53+
debugLog('Using single configuration path...')
54+
55+
const { config, filepath } = await loadConfig({ configPath }, logger)
56+
57+
if (!config) return {}
58+
return { [configPath]: validateConfig(config, filepath, logger) }
59+
}
60+
2861
/** Get all possible config files known to git */
2962
const cachedFiles = parseGitZOutput(await execGit(EXEC_GIT, { cwd: gitDir })).filter(
3063
filterPossibleConfigFiles
@@ -39,8 +72,11 @@ export const searchConfigs = async (gitDir = process.cwd(), logger) => {
3972
const possibleConfigFiles = [...cachedFiles, ...otherFiles]
4073
.map((file) => join(gitDir, file))
4174
.map((file) => normalize(file))
75+
.filter(isInsideDirectory(cwd))
4276
.sort(sortDeepestParth)
4377

78+
debugLog('Found possible config files:', possibleConfigFiles)
79+
4480
/** Create object with key as config file, and value as null */
4581
const configs = possibleConfigFiles.reduce(
4682
(acc, configPath) => Object.assign(acc, { [configPath]: null }),
@@ -65,5 +101,24 @@ export const searchConfigs = async (gitDir = process.cwd(), logger) => {
65101
.filter(([, value]) => !!value)
66102
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
67103

104+
/**
105+
* Try to find a single config from parent directories
106+
* to match old behavior before monorepo support
107+
*/
108+
if (!Object.keys(foundConfigs).length) {
109+
debugLog('Could not find config files inside "%s"', cwd)
110+
111+
const { config, filepath } = await loadConfig({ cwd }, logger)
112+
if (config) {
113+
debugLog('Found parent configuration file from "%s"', filepath)
114+
115+
foundConfigs[filepath] = validateConfig(config, filepath, logger)
116+
} else {
117+
debugLog('Could not find parent configuration files from "%s"', cwd)
118+
}
119+
}
120+
121+
debugLog('Found %d config files', Object.keys(foundConfigs).length)
122+
68123
return foundConfigs
69124
}

0 commit comments

Comments
 (0)