Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use config directory as cwd, when multiple configs present #1091

Merged
merged 2 commits into from Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
okonet marked this conversation as resolved.
Show resolved Hide resolved

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')
iiroj marked this conversation as resolved.
Show resolved Hide resolved

// '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'])
})
})