Skip to content

Commit

Permalink
fix: limit configuration discovery to cwd
Browse files Browse the repository at this point in the history
  • Loading branch information
iiroj committed Apr 20, 2022
1 parent 1b1f0e4 commit d8fdf1d
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 259 deletions.
105 changes: 0 additions & 105 deletions lib/getConfigGroups.js

This file was deleted.

53 changes: 53 additions & 0 deletions lib/groupFilesByConfig.js
@@ -0,0 +1,53 @@
import path from 'path'

import debug from 'debug'

import { ConfigObjectSymbol } from './searchConfigs.js'

const debugLog = debug('lint-staged:groupFilesByConfig')

export const groupFilesByConfig = async ({ configs, files }) => {
debugLog('Grouping %d files by %d configurations', files.length, Object.keys(configs).length)

const filesSet = new Set(files)
const filesByConfig = {}

/** Configs are sorted deepest first by `searchConfigs` */
for (const filepath of Reflect.ownKeys(configs)) {
const config = configs[filepath]

/** When passed an explicit config object via the Node.js API, skip logic */
if (filepath === ConfigObjectSymbol) {
filesByConfig[filepath] = { config, files }
break
}

const dir = path.normalize(path.dirname(filepath))

/** Check if file is inside directory of the configuration file */
const isInsideDir = (file) => {
const relative = path.relative(dir, file)
return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
}

const scopedFiles = new Set()

/**
* If file is inside the config file's directory, assign it to that configuration
* and remove it from the set. This means only one configuration can match a file.
*/
filesSet.forEach((file) => {
if (isInsideDir(file)) {
scopedFiles.add(file)
}
})

scopedFiles.forEach((file) => {
filesSet.delete(file)
})

filesByConfig[filepath] = { config, files: Array.from(scopedFiles) }
}

return filesByConfig
}
19 changes: 16 additions & 3 deletions lib/index.js
@@ -1,9 +1,19 @@
import debug from 'debug'

import { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } from './messages.js'
import {
PREVENTED_EMPTY_COMMIT,
GIT_ERROR,
RESTORE_STASH_EXAMPLE,
NO_CONFIGURATION,
} from './messages.js'
import { printTaskOutput } from './printTaskOutput.js'
import { runAll } from './runAll.js'
import { ApplyEmptyCommitError, GetBackupStashError, GitError } from './symbols.js'
import {
ApplyEmptyCommitError,
ConfigNotFoundError,
GetBackupStashError,
GitError,
} from './symbols.js'
import { validateOptions } from './validateOptions.js'

const debugLog = debug('lint-staged')
Expand Down Expand Up @@ -78,7 +88,10 @@ const lintStaged = async (
} catch (runAllError) {
if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
const { ctx } = runAllError
if (ctx.errors.has(ApplyEmptyCommitError)) {

if (ctx.errors.has(ConfigNotFoundError)) {
logger.error(NO_CONFIGURATION)
} else if (ctx.errors.has(ApplyEmptyCommitError)) {
logger.warn(PREVENTED_EMPTY_COMMIT)
} else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
logger.error(GIT_ERROR)
Expand Down
2 changes: 2 additions & 0 deletions lib/messages.js
Expand Up @@ -22,6 +22,8 @@ export const incorrectBraces = (before, after) =>
`
)

export const NO_CONFIGURATION = `${error} No valid configuration found.`

export const NO_STAGED_FILES = `${info} No staged files found.`

export const NO_TASKS = `${info} No staged files match any configured task.`
Expand Down
27 changes: 15 additions & 12 deletions lib/runAll.js
Expand Up @@ -10,10 +10,10 @@ import normalize from 'normalize-path'
import { chunkFiles } from './chunkFiles.js'
import { execGit } from './execGit.js'
import { generateTasks } from './generateTasks.js'
import { getConfigGroups } from './getConfigGroups.js'
import { getRenderer } from './getRenderer.js'
import { getStagedFiles } from './getStagedFiles.js'
import { GitWorkflow } from './gitWorkflow.js'
import { groupFilesByConfig } from './groupFilesByConfig.js'
import { makeCmdTasks } from './makeCmdTasks.js'
import {
DEPRECATED_GIT_ADD,
Expand All @@ -36,7 +36,7 @@ import {
restoreUnstagedChangesSkipped,
} from './state.js'
import { GitRepoError, GetStagedFilesError, GitError, ConfigNotFoundError } from './symbols.js'
import { searchConfigs } from './searchConfigs.js'
import { ConfigObjectSymbol, searchConfigs } from './searchConfigs.js'

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

Expand Down Expand Up @@ -120,19 +120,16 @@ export const runAll = async (
return ctx
}

const configGroups = await getConfigGroups({ configObject, configPath, cwd, files }, logger)

const hasExplicitConfig = configObject || configPath
const foundConfigs = hasExplicitConfig ? null : await searchConfigs(gitDir, logger)
const numberOfConfigs = hasExplicitConfig ? 1 : Object.keys(foundConfigs).length
const foundConfigs = await searchConfigs({ configObject, configPath, cwd, gitDir }, logger)
const numberOfConfigs = Reflect.ownKeys(foundConfigs).length

// Throw if no configurations were found
if (numberOfConfigs === 0) {
ctx.errors.add(ConfigNotFoundError)
throw createError(ctx, ConfigNotFoundError)
}

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

const hasMultipleConfigs = numberOfConfigs > 1

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

for (const [configPath, { config, files }] of Object.entries(configGroups)) {
const relativeConfig = normalize(path.relative(cwd, configPath))
for (const configPath of Reflect.ownKeys(filesByConfig)) {
const { config, files } = filesByConfig[configPath]

const configName =
configPath === ConfigObjectSymbol
? 'Config object'
: normalize(path.relative(cwd, configPath))

const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })

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

listrTasks.push({
title:
`${relativeConfig}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
`${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
(chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
skip: () => {
// Skip if the first step (backup) failed
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
// Skip chunk when no every task is skipped (due to no matches)
if (chunkListrTasks.every((task) => task.skip())) {
return `${relativeConfig}${dim(' — no tasks to run')}`
return `${configName}${dim(' — no tasks to run')}`
}
return false
},
Expand Down
63 changes: 59 additions & 4 deletions lib/searchConfigs.js
Expand Up @@ -2,13 +2,16 @@

import { basename, join } from 'path'

import debug from 'debug'
import normalize from 'normalize-path'

import { execGit } from './execGit.js'
import { loadConfig, searchPlaces } from './loadConfig.js'
import { parseGitZOutput } from './parseGitZOutput.js'
import { validateConfig } from './validateConfig.js'

const debugLog = debug('lint-staged:searchConfigs')

const EXEC_GIT = ['ls-files', '-z', '--full-name']

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

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

const isInsideDirectory = (dir) => (file) => file.startsWith(normalize(dir))

export const ConfigObjectSymbol = Symbol()

/**
* Search all config files from the git repository
* Search all config files from the git repository, preferring those inside `cwd`.
*
* @param {string} gitDir
* @param {object} options
* @param {Object} [options.configObject] - Explicit config object from the js API
* @param {string} [options.configPath] - Explicit path to a config file
* @param {string} [options.cwd] - Current working directory
* @param {Logger} logger
* @returns {Promise<{ [key: string]: * }>} found configs with filepath as key, and config as value
*
* @returns {Promise<{ [key: string]: { config: *, files: string[] } }>} found configs with filepath as key, and config as value
*/
export const searchConfigs = async (gitDir = process.cwd(), logger) => {
export const searchConfigs = async (
{ configObject, configPath, cwd = process.cwd(), gitDir = cwd },
logger
) => {
debugLog('Searching for configuration files...')

// Return explicit config object from js API
if (configObject) {
debugLog('Using single direct configuration object...')

return { [ConfigObjectSymbol]: validateConfig(configObject, 'config object', logger) }
}

// Use only explicit config path instead of discovering multiple
if (configPath) {
debugLog('Using single configuration path...')

const { config, filepath } = await loadConfig({ configPath }, logger)

if (!config) return {}
return { [configPath]: validateConfig(config, filepath, logger) }
}

/** Get all possible config files known to git */
const cachedFiles = parseGitZOutput(await execGit(EXEC_GIT, { cwd: gitDir })).filter(
filterPossibleConfigFiles
Expand All @@ -39,8 +72,11 @@ export const searchConfigs = async (gitDir = process.cwd(), logger) => {
const possibleConfigFiles = [...cachedFiles, ...otherFiles]
.map((file) => join(gitDir, file))
.map((file) => normalize(file))
.filter(isInsideDirectory(cwd))
.sort(sortDeepestParth)

debugLog('Found possible config files:', possibleConfigFiles)

/** Create object with key as config file, and value as null */
const configs = possibleConfigFiles.reduce(
(acc, configPath) => Object.assign(acc, { [configPath]: null }),
Expand All @@ -65,5 +101,24 @@ export const searchConfigs = async (gitDir = process.cwd(), logger) => {
.filter(([, value]) => !!value)
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})

/**
* Try to find a single config from parent directories
* to match old behavior before monorepo support
*/
if (!Object.keys(foundConfigs).length) {
debugLog('Could not find config files inside "%s"', cwd)

const { config, filepath } = await loadConfig({ cwd }, logger)
if (config) {
debugLog('Found parent configuration file from "%s"', filepath)

foundConfigs[filepath] = validateConfig(config, filepath, logger)
} else {
debugLog('Could not find parent configuration files from "%s"', cwd)
}
}

debugLog('Found %d config files', Object.keys(foundConfigs).length)

return foundConfigs
}

0 comments on commit d8fdf1d

Please sign in to comment.