diff --git a/.lintstagedrc.json b/.lintstagedrc.json index 144f4ebbf..37184e8d6 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,5 +1,4 @@ { "*.{js,json,md}": "prettier --write", - "*.js": "npm run lint:base --fix", - ".*{rc, json}": "jsonlint --in-place" + "*.js": "npm run lint:base -- --fix" } diff --git a/README.md b/README.md index 294ba0499..a267b8f19 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,19 @@ See [examples](#examples) and [configuration](#configuration) for more informati See [Releases](https://github.com/okonet/lint-staged/releases) +### Migration + +#### v10 + +- From `v10.0.0` onwards any new modifications to originally staged files will be automatically added to the commit. + If your task previously contained a `git add` step, please remove this. + The automatic behaviour ensures there are less race-conditions, + since trying to run multiple git operations at the same time usually results in an error. +- From `v10.0.0` onwards _lint-staged_ uses git stashes to improve speed and provide backups while running. + Since git stashes require at least an initial commit, you shouldn't run _lint-staged_ in an empty repo. +- From `v10.0.0` onwards _lint-staged_ requires Node.js version 10.13.0 or later. +- From `v10.0.0` onwards _lint-staged_ will abort the commit if linter tasks undo all staged changes. To allow creating empty commit, please use the `--allow-empty` option. + ## Command line flags ```bash @@ -47,7 +60,7 @@ Usage: lint-staged [options] Options: -V, --version output the version number - --allow-empty allow empty commits when tasks revert all staged changes (default: false) + --allow-empty allow empty commits when tasks undo all staged changes (default: false) -c, --config [path] path to configuration file -d, --debug print additional debug information (default: false) -p, --concurrent the number of tasks to run concurrently, or false to run tasks serially (default: true) @@ -57,7 +70,7 @@ Options: -h, --help output usage information ``` -- **`--allow-empty`**: By default, when after running tasks there are no staged modifications, lint-staged will exit with an error and abort the commit. Use this flag to allow creating empty git commits. +- **`--allow-empty`**: By default, when linter tasks undo all staged changes, lint-staged will exit with an error and abort the commit. Use this flag to allow creating empty git commits. - **`--config [path]`**: Manually specify a path to a config file or npm package name. Note: when used, lint-staged won't perform the config file search and print an error if the specified file cannot be found. - **`--debug`**: Run in debug mode. When set, it does the following: - uses [debug](https://github.com/visionmedia/debug) internally to log additional information about staged files, commands being executed, location of binaries, etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`. @@ -499,6 +512,7 @@ const { CLIEngine } = require('eslint') const cli = new CLIEngine({}) module.exports = { - '*.js': files => 'eslint --max-warnings=0 ' + files.filter( file => ! cli.isPathIgnored( file ) ).join( ' ' ) + '*.js': files => + 'eslint --max-warnings=0 ' + files.filter(file => !cli.isPathIgnored(file)).join(' ') } ``` diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index 25c947730..31b1e77e9 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -111,7 +111,7 @@ class GitWorkflow { // Save stash of entire original state, including unstaged and untracked changes. // `--keep-index leaves only staged files on disk, for tasks.` - await this.execGit(['stash', 'save', '--quiet', '--include-untracked', '--keep-index', STASH]) + await this.execGit(['stash', 'save', '--include-untracked', '--keep-index', STASH]) // Restore meta information about ongoing git merge await this.restoreMergeStatus() @@ -134,6 +134,9 @@ class GitWorkflow { debug('Done backing up original state!') } catch (error) { + if (error.message && error.message.includes('You do not have the initial commit yet')) { + ctx.emptyGitRepo = true + } handleError(error, ctx) } } diff --git a/lib/resolveTaskFn.js b/lib/resolveTaskFn.js index 44c23db89..e0caf8948 100644 --- a/lib/resolveTaskFn.js +++ b/lib/resolveTaskFn.js @@ -1,6 +1,7 @@ 'use strict' const chalk = require('chalk') +const { parseArgsStringToArgv } = require('string-argv') const dedent = require('dedent') const execa = require('execa') const symbols = require('log-symbols') @@ -66,13 +67,14 @@ function makeErr(linter, result, context = {}) { * @returns {function(): Promise>} */ module.exports = function resolveTaskFn({ command, files, gitDir, isFn, relative, shell = false }) { - const cmd = isFn ? command : `${command} ${files.join(' ')}` + const [cmd, ...args] = parseArgsStringToArgv(command) debug('cmd:', cmd) + debug('args:', args) const execaOptions = { preferLocal: true, reject: false, shell } if (relative) { execaOptions.cwd = process.cwd() - } else if (/^git(\.exe)?/i.test(command) && gitDir !== process.cwd()) { + } else if (/^git(\.exe)?/i.test(cmd) && gitDir !== process.cwd()) { // Only use gitDir as CWD if we are using the git binary // e.g `npm` should run tasks in the actual CWD execaOptions.cwd = gitDir @@ -80,12 +82,15 @@ module.exports = function resolveTaskFn({ command, files, gitDir, isFn, relative debug('execaOptions:', execaOptions) return async ctx => { - const result = await execa.command(cmd, execaOptions) + const promise = shell + ? execa.command(isFn ? command : `${command} ${files.join(' ')}`, execaOptions) + : execa(cmd, isFn ? args : args.concat(files), execaOptions) + const result = await promise if (result.failed || result.killed || result.signal != null) { - throw makeErr(command, result, ctx) + throw makeErr(cmd, result, ctx) } - return successMsg(command) + return successMsg(cmd) } } diff --git a/lib/runAll.js b/lib/runAll.js index 10ce9a26e..735c88d66 100644 --- a/lib/runAll.js +++ b/lib/runAll.js @@ -85,7 +85,7 @@ module.exports = async function runAll( shell }) - if (subTasks.some(subTask => subTask.command.includes('git add'))) { + if (subTasks.some(subTask => subTask.command === 'git add')) { hasDeprecatedGitAdd = true } @@ -187,19 +187,22 @@ module.exports = async function runAll( ${symbols.warning} ${chalk.yellow(`lint-staged prevented an empty git commit. Use the --allow-empty option to continue, or check your task configuration`)} `) - } - - // Show help text about manual restore in case of git errors. - // No sense to show this if the backup stash itself is missing. - else if (error.context.gitError && !error.context.gitGetBackupStashError) { - logger.error(` - ${symbols.error} ${chalk.red(`lint-staged failed due to a git error. - Any lost modifications can be restored from a git stash: + } else if (error.context.gitError && !error.context.gitGetBackupStashError) { + logger.error(`\n ${symbols.error} ${chalk.red(`lint-staged failed due to a git error.`)}`) + + if (error.context.emptyGitRepo) { + logger.error( + `\n The initial commit is needed for lint-staged to work. + Please use the --no-verify flag to skip running lint-staged.` + ) + } else { + // No sense to show this if the backup stash itself is missing. + logger.error(` Any lost modifications can be restored from a git stash: > git stash list stash@{0}: On master: automatic lint-staged backup - > git stash pop stash@{0}`)} -`) + > git stash pop stash@{0}\n`) + } } throw error diff --git a/package.json b/package.json index 34e6f8e59..f5c669df0 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "micromatch": "^4.0.2", "normalize-path": "^3.0.0", "please-upgrade-node": "^3.2.0", + "string-argv": "0.3.1", "stringify-object": "^3.3.0" }, "devDependencies": { @@ -67,7 +68,6 @@ "husky": "^3.1.0", "jest": "^24.9.0", "jest-snapshot-serializer-ansi": "^1.0.0", - "jsonlint": "^1.6.3", "nanoid": "^2.1.7", "prettier": "^1.19.1" }, diff --git a/test/__snapshots__/index.spec.js.snap b/test/__snapshots__/index.spec.js.snap index ebf095066..adcb57ecc 100644 --- a/test/__snapshots__/index.spec.js.snap +++ b/test/__snapshots__/index.spec.js.snap @@ -5,7 +5,7 @@ exports[`lintStaged should exit with code 1 on linter errors 1`] = ` ERROR -× node -e \\"process.exit(1)\\" found some errors. Please fix them and try committing again." +× node found some errors. Please fix them and try committing again." `; exports[`lintStaged should load an npm config package when specified 1`] = ` diff --git a/test/__snapshots__/runAll.spec.js.snap b/test/__snapshots__/runAll.spec.js.snap index 29ac4200d..23c713ac5 100644 --- a/test/__snapshots__/runAll.spec.js.snap +++ b/test/__snapshots__/runAll.spec.js.snap @@ -44,7 +44,7 @@ LOG { name: 'ListrError', errors: [ { - privateMsg: '\\\\n\\\\n\\\\n× echo \\"sample\\" found some errors. Please fix them and try committing again.\\\\n\\\\nLinter finished with error', + privateMsg: '\\\\n\\\\n\\\\n× echo found some errors. Please fix them and try committing again.\\\\n\\\\nLinter finished with error', context: {taskError: true} } ], @@ -75,7 +75,7 @@ LOG { name: 'ListrError', errors: [ { - privateMsg: '\\\\n\\\\n\\\\n‼ echo \\"sample\\" was terminated with SIGINT', + privateMsg: '\\\\n\\\\n\\\\n‼ echo was terminated with SIGINT', context: {taskError: true} } ], diff --git a/test/makeCmdTasks.spec.js b/test/makeCmdTasks.spec.js index 393f820b2..88d675482 100644 --- a/test/makeCmdTasks.spec.js +++ b/test/makeCmdTasks.spec.js @@ -42,7 +42,7 @@ describe('makeCmdTasks', () => { expect(taskPromise).toBeInstanceOf(Promise) await taskPromise expect(execa).toHaveBeenCalledTimes(1) - expect(execa).lastCalledWith('test test.js', { + expect(execa).lastCalledWith('test', ['test.js'], { preferLocal: true, reject: false, shell: false @@ -51,7 +51,7 @@ describe('makeCmdTasks', () => { expect(taskPromise).toBeInstanceOf(Promise) await taskPromise expect(execa).toHaveBeenCalledTimes(2) - expect(execa).lastCalledWith('test2 test.js', { + expect(execa).lastCalledWith('test2', ['test.js'], { preferLocal: true, reject: false, shell: false diff --git a/test/resolveTaskFn.spec.js b/test/resolveTaskFn.spec.js index bcfefc7dc..04612d5a2 100644 --- a/test/resolveTaskFn.spec.js +++ b/test/resolveTaskFn.spec.js @@ -18,7 +18,7 @@ describe('resolveTaskFn', () => { await taskFn() expect(execa).toHaveBeenCalledTimes(1) - expect(execa).lastCalledWith('node --arg=true ./myscript.js test.js', { + expect(execa).lastCalledWith('node', ['--arg=true', './myscript.js', 'test.js'], { preferLocal: true, reject: false, shell: false @@ -35,7 +35,7 @@ describe('resolveTaskFn', () => { await taskFn() expect(execa).toHaveBeenCalledTimes(1) - expect(execa).lastCalledWith('node --arg=true ./myscript.js test.js', { + expect(execa).lastCalledWith('node', ['--arg=true', './myscript.js', 'test.js'], { preferLocal: true, reject: false, shell: false @@ -87,7 +87,7 @@ describe('resolveTaskFn', () => { await taskFn() expect(execa).toHaveBeenCalledTimes(1) - expect(execa).lastCalledWith('git diff test.js', { + expect(execa).lastCalledWith('git', ['diff', 'test.js'], { cwd: '../', preferLocal: true, reject: false, @@ -101,7 +101,7 @@ describe('resolveTaskFn', () => { await taskFn() expect(execa).toHaveBeenCalledTimes(1) - expect(execa).lastCalledWith('jest test.js', { + expect(execa).lastCalledWith('jest', ['test.js'], { preferLocal: true, reject: false, shell: false @@ -118,7 +118,7 @@ describe('resolveTaskFn', () => { await taskFn() expect(execa).toHaveBeenCalledTimes(1) - expect(execa).lastCalledWith('git diff test.js', { + expect(execa).lastCalledWith('git', ['diff', 'test.js'], { cwd: process.cwd(), preferLocal: true, reject: false, diff --git a/test/resolveTaskFn.unmocked.spec.js b/test/resolveTaskFn.unmocked.spec.js index e41589ba8..c2141a471 100644 --- a/test/resolveTaskFn.unmocked.spec.js +++ b/test/resolveTaskFn.unmocked.spec.js @@ -11,8 +11,6 @@ describe('resolveTaskFn', () => { shell: true }) - await expect(taskFn()).resolves.toMatchInlineSnapshot( - `"√ node -e \\"process.exit(1)\\" || echo $? passed!"` - ) + await expect(taskFn()).resolves.toMatchInlineSnapshot(`"√ node passed!"`) }) }) diff --git a/test/runAll.unmocked.2.spec.js b/test/runAll.unmocked.2.spec.js index 47b7805a0..202449e33 100644 --- a/test/runAll.unmocked.2.spec.js +++ b/test/runAll.unmocked.2.spec.js @@ -120,7 +120,7 @@ describe('runAll', () => { LOG → Skipped because of previous git error. ERROR × lint-staged failed due to a git error. - Any lost modifications can be restored from a git stash: + ERROR Any lost modifications can be restored from a git stash: > git stash list stash@{0}: On master: automatic lint-staged backup diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js index ea197ff9e..36d60fcb1 100644 --- a/test/runAll.unmocked.spec.js +++ b/test/runAll.unmocked.spec.js @@ -121,8 +121,8 @@ describe('runAll', () => { it('Should commit entire staged file when no errors from linter', async () => { // Stage pretty file - await appendFile('test.js', testJsFilePretty) - await execGit(['add', 'test.js']) + await appendFile('test file.js', testJsFilePretty) + await execGit(['add', 'test file.js']) // Run lint-staged with `prettier --list-different` and commit pretty file await gitCommit({ config: { '*.js': 'prettier --list-different' } }) @@ -130,7 +130,7 @@ describe('runAll', () => { // Nothing is wrong, so a new commit is created expect(await execGit(['rev-list', '--count', 'HEAD'])).toEqual('2') expect(await execGit(['log', '-1', '--pretty=%B'])).toMatch('test') - expect(await readFile('test.js')).toEqual(testJsFilePretty) + expect(await readFile('test file.js')).toEqual(testJsFilePretty) }) it('Should commit entire staged file when no errors and linter modifies file', async () => { @@ -381,11 +381,9 @@ describe('runAll', () => { ).rejects.toThrowError() expect(console.printHistory()).toMatchInlineSnapshot(` " - WARN ‼ Some of your tasks use \`git add\` command. Please remove it from the config since all modifications made by tasks will be automatically added to the git commit index. - ERROR × lint-staged failed due to a git error. - Any lost modifications can be restored from a git stash: + ERROR Any lost modifications can be restored from a git stash: > git stash list stash@{0}: On master: automatic lint-staged backup @@ -693,12 +691,15 @@ describe('runAll', () => { // Run lint-staged with prettier --write to automatically fix the file // Since prettier reverts all changes, the commit should fail + // use the old syntax with manual `git add` to provide a warning message await expect( - gitCommit({ config: { '*.js': 'prettier --write' } }) + gitCommit({ config: { '*.js': ['prettier --write', 'git add'] } }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Something went wrong"`) expect(console.printHistory()).toMatchInlineSnapshot(` " + WARN ‼ Some of your tasks use \`git add\` command. Please remove it from the config since all modifications made by tasks will be automatically added to the git commit index. + WARN ‼ lint-staged prevented an empty git commit. Use the --allow-empty option to continue, or check your task configuration @@ -773,3 +774,28 @@ describe('runAll', () => { expect(await readFile('test.js', submoduleDir)).toEqual(testJsFilePretty) }) }) + +describe('runAll', () => { + it('Should throw when run on an empty git repo without an initial commit', async () => { + const tmpDir = await createTempDir() + const cwd = normalize(tmpDir) + const logger = makeConsoleMock() + + await execGit('init', { cwd }) + await execGit(['config', 'user.name', '"test"'], { cwd }) + await execGit(['config', 'user.email', '"test@test.com"'], { cwd }) + await appendFile('test.js', testJsFilePretty, cwd) + await execGit(['add', 'test.js'], { cwd }) + await expect( + runAll({ config: { '*.js': 'prettier --list-different' }, cwd, quiet: true }, logger) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Something went wrong"`) + expect(logger.printHistory()).toMatchInlineSnapshot(` + " + ERROR + × lint-staged failed due to a git error. + ERROR + The initial commit is needed for lint-staged to work. + Please use the --no-verify flag to skip running lint-staged." + `) + }) +}) diff --git a/yarn.lock b/yarn.lock index f892eb965..319b483d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1012,11 +1012,6 @@ dependencies: "@types/yargs-parser" "*" -JSV@^4.0.x: - version "4.0.2" - resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57" - integrity sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c= - abab@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" @@ -1129,11 +1124,6 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" -ansi-styles@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" - integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg= - any-observable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -1519,15 +1509,6 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" - integrity sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8= - dependencies: - ansi-styles "~1.0.0" - has-color "~0.1.0" - strip-ansi "~0.1.0" - chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -2733,11 +2714,6 @@ has-ansi@^3.0.0: dependencies: ansi-regex "^3.0.0" -has-color@~0.1.0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" - integrity sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8= - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -3705,14 +3681,6 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" -jsonlint@^1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.3.tgz#cb5e31efc0b78291d0d862fbef05900adf212988" - integrity sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A== - dependencies: - JSV "^4.0.x" - nomnom "^1.5.x" - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -4179,14 +4147,6 @@ node-releases@^1.1.38: dependencies: semver "^6.3.0" -nomnom@^1.5.x: - version "1.8.1" - resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" - integrity sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc= - dependencies: - chalk "~0.4.0" - underscore "~1.6.0" - nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" @@ -5345,6 +5305,11 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= +string-argv@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" + integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -5448,11 +5413,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" - integrity sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE= - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -5699,11 +5659,6 @@ uglify-js@^3.1.4: commander "~2.20.3" source-map "~0.6.1" -underscore@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" - integrity sha1-izixDKze9jM3uLJOT/htRa6lKag= - unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"