Skip to content

Commit

Permalink
fix: use config directory as cwd, when multiple configs present (#1091)
Browse files Browse the repository at this point in the history
  • Loading branch information
iiroj committed Feb 1, 2022
1 parent 3a897ff commit 9a14e92
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 6 deletions.
26 changes: 20 additions & 6 deletions lib/runAll.js
Expand Up @@ -80,7 +80,8 @@ export const runAll = async (
debugLog('Running all linter scripts...')

// Resolve relative CWD option
cwd = cwd ? path.resolve(cwd) : process.cwd()
const hasExplicitCwd = !!cwd
cwd = hasExplicitCwd ? path.resolve(cwd) : process.cwd()
debugLog('Using working directory `%s`', cwd)

const ctx = getInitialState({ quiet })
Expand Down Expand Up @@ -120,6 +121,8 @@ export const runAll = async (

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

const hasMultipleConfigs = Object.keys(configGroups).length > 1

// lint-staged 10 will automatically add modifications to index
// Warn user when their command includes `git add`
let hasDeprecatedGitAdd = false
Expand All @@ -138,21 +141,25 @@ export const runAll = async (
const matchedFiles = new Set()

for (const [configPath, { config, files }] of Object.entries(configGroups)) {
const relativeConfig = 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.
// Otherwise use the directory of the config file for each config group,
// to make sure tasks are separated from each other.
const groupCwd = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd

const chunkCount = stagedFileChunks.length
if (chunkCount > 1) {
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
}

for (const [index, files] of stagedFileChunks.entries()) {
const relativeConfig = normalize(path.relative(cwd, configPath))

const chunkListrTasks = await Promise.all(
generateTasks({ config, cwd, files, relative }).map((task) =>
generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
makeCmdTasks({
commands: task.commands,
cwd,
cwd: groupCwd,
files: task.fileList,
gitDir,
renderer: listrOptions.renderer,
Expand All @@ -161,7 +168,14 @@ export const runAll = async (
}).then((subTasks) => {
// Add files from task to match set
task.fileList.forEach((file) => {
matchedFiles.add(file)
// Make sure relative files are normalized to the
// group cwd, because other there might be identical
// relative filenames in the entire set.
const normalizedFile = path.isAbsolute(file)
? file
: normalize(path.join(groupCwd, file))

matchedFiles.add(normalizedFile)
})

hasDeprecatedGitAdd =
Expand Down
38 changes: 38 additions & 0 deletions test/integration.test.js
Expand Up @@ -1149,6 +1149,44 @@ describe('lint-staged', () => {
expect(await readFile('a/very/deep/file/path/file.js')).toMatch('level-0')
})

it('should support multiple configuration files with --relative', async () => {
// Add some empty files
await writeFile('file.js', '')
await writeFile('deeper/file.js', '')
await writeFile('deeper/even/file.js', '')
await writeFile('deeper/even/deeper/file.js', '')
await writeFile('a/very/deep/file/path/file.js', '')

const echoJSConfig = `module.exports = { '*.js': (files) => files.map((f) => \`echo \${f} > \${f}\`) }`

await writeFile('.lintstagedrc.js', echoJSConfig)
await writeFile('deeper/.lintstagedrc.js', echoJSConfig)
await writeFile('deeper/even/.lintstagedrc.cjs', echoJSConfig)

// Stage all files
await execGit(['add', '.'])

// Run lint-staged with `--shell` so that tasks do their thing
await gitCommit({ relative: true, shell: true })

// 'file.js' is relative to '.'
expect(await readFile('file.js')).toMatch('file.js')

// 'deeper/file.js' is relative to 'deeper/'
expect(await readFile('deeper/file.js')).toMatch('file.js')

// 'deeper/even/file.js' is relative to 'deeper/even/'
expect(await readFile('deeper/even/file.js')).toMatch('file.js')

// 'deeper/even/deeper/file.js' is relative to parent 'deeper/even/'
expect(await readFile('deeper/even/deeper/file.js')).toMatch(normalize('deeper/file.js'))

// 'a/very/deep/file/path/file.js' is relative to root '.'
expect(await readFile('a/very/deep/file/path/file.js')).toMatch(
normalize('a/very/deep/file/path/file.js')
)
})

it('should not care about staged file outside current cwd with another staged file', async () => {
await writeFile('file.js', testJsFileUgly)
await writeFile('deeper/file.js', testJsFileUgly)
Expand Down
75 changes: 75 additions & 0 deletions test/runAll.spec.js
Expand Up @@ -9,6 +9,7 @@ import { GitWorkflow } from '../lib/gitWorkflow'
import { resolveGitRepo } from '../lib/resolveGitRepo'
import { runAll } from '../lib/runAll'
import { GitError } from '../lib/symbols'
import * as getConfigGroupsNS from '../lib/getConfigGroups'

jest.mock('../lib/file')
jest.mock('../lib/getStagedFiles')
Expand All @@ -26,6 +27,8 @@ jest.mock('../lib/resolveConfig', () => ({
},
}))

const getConfigGroups = jest.spyOn(getConfigGroupsNS, 'getConfigGroups')

getStagedFiles.mockImplementation(async () => [])

resolveGitRepo.mockImplementation(async () => {
Expand Down Expand Up @@ -293,4 +296,76 @@ describe('runAll', () => {
expect(mockConstructor).toHaveBeenCalledTimes(1)
expect(expected).toEqual([[normalize(path.join(cwd, 'test/foo.js'))]])
})

it('should resolve matched files to config locations with multiple configs', async () => {
getStagedFiles.mockImplementationOnce(async () => ['foo.js', 'test/foo.js'])

const mockTask = jest.fn(() => ['echo "sample"'])

getConfigGroups.mockResolvedValueOnce({
'.lintstagedrc.json': {
config: { '*.js': mockTask },
files: ['foo.js'],
},
'test/.lintstagedrc.json': {
config: { '*.js': mockTask },
files: ['test/foo.js'],
},
})

// We are only interested in the `matchedFileChunks` generation
let expected
const mockConstructor = jest.fn(({ matchedFileChunks }) => (expected = matchedFileChunks))
GitWorkflow.mockImplementationOnce(mockConstructor)

try {
await runAll({
stash: false,
relative: true,
})
} catch {} // eslint-disable-line no-empty

// task received relative `foo.js` from both directories
expect(mockTask).toHaveBeenCalledTimes(2)
expect(mockTask).toHaveBeenNthCalledWith(1, ['foo.js'])
expect(mockTask).toHaveBeenNthCalledWith(2, ['foo.js'])
// GitWorkflow received absolute paths `foo.js` and `test/foo.js`
expect(mockConstructor).toHaveBeenCalledTimes(1)
expect(expected).toEqual([
[
normalize(path.join(process.cwd(), 'foo.js')),
normalize(path.join(process.cwd(), 'test/foo.js')),
],
])
})

it('should resolve matched files to explicit cwd with multiple configs', async () => {
getStagedFiles.mockImplementationOnce(async () => ['foo.js', 'test/foo.js'])

const mockTask = jest.fn(() => ['echo "sample"'])

getConfigGroups.mockResolvedValueOnce({
'.lintstagedrc.json': {
config: { '*.js': mockTask },
files: ['foo.js'],
},
'test/.lintstagedrc.json': {
config: { '*.js': mockTask },
files: ['test/foo.js'],
},
})

try {
await runAll({
cwd: '.',
stash: false,
relative: true,
})
} catch {} // eslint-disable-line no-empty

expect(mockTask).toHaveBeenCalledTimes(2)
expect(mockTask).toHaveBeenNthCalledWith(1, ['foo.js'])
// This is now relative to "." instead of "test/"
expect(mockTask).toHaveBeenNthCalledWith(2, ['test/foo.js'])
})
})

0 comments on commit 9a14e92

Please sign in to comment.