lint-staged 10.1.6 → 10.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -17
- package/bin/lint-staged.js +9 -3
- package/lib/chunkFiles.js +6 -5
- package/lib/execGit.js +1 -1
- package/lib/file.js +1 -1
- package/lib/generateTasks.js +2 -2
- package/lib/getRenderer.js +12 -0
- package/lib/gitWorkflow.js +47 -47
- package/lib/index.js +40 -10
- package/lib/makeCmdTasks.js +3 -2
- package/lib/messages.js +54 -0
- package/lib/printTaskOutput.js +16 -0
- package/lib/resolveTaskFn.js +58 -39
- package/lib/runAll.js +83 -110
- package/lib/state.js +89 -0
- package/lib/symbols.js +25 -0
- package/lib/validateConfig.js +1 -1
- package/package.json +4 -10
- package/lib/printErrors.js +0 -15
package/README.md
CHANGED
|
@@ -72,7 +72,9 @@ Options:
|
|
|
72
72
|
-r, --relative pass relative filepaths to tasks (default: false)
|
|
73
73
|
-x, --shell skip parsing of tasks for better shell support (default:
|
|
74
74
|
false)
|
|
75
|
-
-
|
|
75
|
+
-v, --verbose show task output even when tasks succeed; by default only
|
|
76
|
+
failed output is shown (default: false)
|
|
77
|
+
-h, --help display help for command
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
- **`--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.
|
|
@@ -88,6 +90,7 @@ Options:
|
|
|
88
90
|
- **`--quiet`**: Supress all CLI output, except from tasks.
|
|
89
91
|
- **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
|
|
90
92
|
- **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option.
|
|
93
|
+
- **`--verbose`**: Show task output even when tasks succeed. By default only failed output is shown.
|
|
91
94
|
|
|
92
95
|
## Configuration
|
|
93
96
|
|
|
@@ -199,7 +202,7 @@ type TaskFn = (filenames: string[]) => string | string[] | Promise<string | stri
|
|
|
199
202
|
```js
|
|
200
203
|
// .lintstagedrc.js
|
|
201
204
|
module.exports = {
|
|
202
|
-
'**/*.js?(x)': filenames => filenames.map(filename => `prettier --write '${filename}'`)
|
|
205
|
+
'**/*.js?(x)': (filenames) => filenames.map((filename) => `prettier --write '${filename}'`)
|
|
203
206
|
}
|
|
204
207
|
```
|
|
205
208
|
|
|
@@ -217,7 +220,8 @@ module.exports = {
|
|
|
217
220
|
```js
|
|
218
221
|
// .lintstagedrc.js
|
|
219
222
|
module.exports = {
|
|
220
|
-
'**/*.js?(x)': filenames =>
|
|
223
|
+
'**/*.js?(x)': (filenames) =>
|
|
224
|
+
filenames.length > 10 ? 'eslint .' : `eslint ${filenames.join(' ')}`
|
|
221
225
|
}
|
|
222
226
|
```
|
|
223
227
|
|
|
@@ -228,7 +232,7 @@ module.exports = {
|
|
|
228
232
|
const micromatch = require('micromatch')
|
|
229
233
|
|
|
230
234
|
module.exports = {
|
|
231
|
-
'*': allFiles => {
|
|
235
|
+
'*': (allFiles) => {
|
|
232
236
|
const match = micromatch(allFiles, ['*.js', '*.ts'])
|
|
233
237
|
return `eslint ${match.join(' ')}`
|
|
234
238
|
}
|
|
@@ -244,7 +248,7 @@ If for some reason you want to ignore files from the glob match, you can use `mi
|
|
|
244
248
|
const micromatch = require('micromatch')
|
|
245
249
|
|
|
246
250
|
module.exports = {
|
|
247
|
-
'*.js': files => {
|
|
251
|
+
'*.js': (files) => {
|
|
248
252
|
// from `files` filter those _NOT_ matching `*test.js`
|
|
249
253
|
const match = micromatch.not(files, '*test.js')
|
|
250
254
|
return `eslint ${match.join(' ')}`
|
|
@@ -260,9 +264,9 @@ Please note that for most cases, globs can achieve the same effect. For the abov
|
|
|
260
264
|
const path = require('path')
|
|
261
265
|
|
|
262
266
|
module.exports = {
|
|
263
|
-
'*.ts': absolutePaths => {
|
|
267
|
+
'*.ts': (absolutePaths) => {
|
|
264
268
|
const cwd = process.cwd()
|
|
265
|
-
const relativePaths = absolutePaths.map(file => path.relative(cwd, file))
|
|
269
|
+
const relativePaths = absolutePaths.map((file) => path.relative(cwd, file))
|
|
266
270
|
return `ng lint myProjectName --files ${relativePaths.join(' ')}`
|
|
267
271
|
}
|
|
268
272
|
}
|
|
@@ -422,12 +426,17 @@ Parameters to `lintStaged` are equivalent to their CLI counterparts:
|
|
|
422
426
|
|
|
423
427
|
```js
|
|
424
428
|
const success = await lintStaged({
|
|
429
|
+
allowEmpty: false,
|
|
430
|
+
concurrent: true,
|
|
425
431
|
configPath: './path/to/configuration/file',
|
|
432
|
+
cwd: process.cwd(),
|
|
433
|
+
debug: false,
|
|
426
434
|
maxArgLength: null,
|
|
427
|
-
relative: false,
|
|
428
|
-
shell: false,
|
|
429
435
|
quiet: false,
|
|
430
|
-
|
|
436
|
+
relative: false,
|
|
437
|
+
shell: false
|
|
438
|
+
stash: true,
|
|
439
|
+
verbose: false
|
|
431
440
|
})
|
|
432
441
|
```
|
|
433
442
|
|
|
@@ -435,14 +444,17 @@ You can also pass config directly with `config` option:
|
|
|
435
444
|
|
|
436
445
|
```js
|
|
437
446
|
const success = await lintStaged({
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
},
|
|
447
|
+
allowEmpty: false,
|
|
448
|
+
concurrent: true,
|
|
449
|
+
config: { '*.js': 'eslint --fix' },
|
|
450
|
+
cwd: process.cwd(),
|
|
451
|
+
debug: false,
|
|
441
452
|
maxArgLength: null,
|
|
453
|
+
quiet: false,
|
|
442
454
|
relative: false,
|
|
443
455
|
shell: false,
|
|
444
|
-
|
|
445
|
-
|
|
456
|
+
stash: true,
|
|
457
|
+
verbose: false
|
|
446
458
|
})
|
|
447
459
|
```
|
|
448
460
|
|
|
@@ -518,7 +530,7 @@ const { CLIEngine } = require('eslint')
|
|
|
518
530
|
const cli = new CLIEngine({})
|
|
519
531
|
|
|
520
532
|
module.exports = {
|
|
521
|
-
'*.js': files =>
|
|
522
|
-
'eslint --max-warnings=0 ' + files.filter(file => !cli.isPathIgnored(file)).join(' ')
|
|
533
|
+
'*.js': (files) =>
|
|
534
|
+
'eslint --max-warnings=0 ' + files.filter((file) => !cli.isPathIgnored(file)).join(' ')
|
|
523
535
|
}
|
|
524
536
|
```
|
package/bin/lint-staged.js
CHANGED
|
@@ -15,8 +15,8 @@ const pkg = require('../package.json')
|
|
|
15
15
|
require('please-upgrade-node')(
|
|
16
16
|
Object.assign({}, pkg, {
|
|
17
17
|
engines: {
|
|
18
|
-
node: '>=10.13.0' // First LTS release of 'Dubnium'
|
|
19
|
-
}
|
|
18
|
+
node: '>=10.13.0', // First LTS release of 'Dubnium'
|
|
19
|
+
},
|
|
20
20
|
})
|
|
21
21
|
)
|
|
22
22
|
|
|
@@ -40,6 +40,11 @@ cmdline
|
|
|
40
40
|
.option('-q, --quiet', 'disable lint-staged’s own console output', false)
|
|
41
41
|
.option('-r, --relative', 'pass relative filepaths to tasks', false)
|
|
42
42
|
.option('-x, --shell', 'skip parsing of tasks for better shell support', false)
|
|
43
|
+
.option(
|
|
44
|
+
'-v, --verbose',
|
|
45
|
+
'show task output even when tasks succeed; by default only failed output is shown',
|
|
46
|
+
false
|
|
47
|
+
)
|
|
43
48
|
.parse(process.argv)
|
|
44
49
|
|
|
45
50
|
if (cmdline.debug) {
|
|
@@ -75,7 +80,8 @@ const options = {
|
|
|
75
80
|
stash: !!cmdline.stash, // commander inverts `no-<x>` flags to `!x`
|
|
76
81
|
quiet: !!cmdline.quiet,
|
|
77
82
|
relative: !!cmdline.relative,
|
|
78
|
-
shell: !!cmdline.shell
|
|
83
|
+
shell: !!cmdline.shell,
|
|
84
|
+
verbose: !!cmdline.verbose,
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
debug('Options parsed from command-line:', options)
|
package/lib/chunkFiles.js
CHANGED
|
@@ -33,19 +33,20 @@ function chunkArray(arr, chunkCount) {
|
|
|
33
33
|
* @returns {Array<Array<String>>}
|
|
34
34
|
*/
|
|
35
35
|
module.exports = function chunkFiles({ files, baseDir, maxArgLength = null, relative = false }) {
|
|
36
|
+
const normalizedFiles = files.map((file) =>
|
|
37
|
+
normalize(relative || !baseDir ? file : path.resolve(baseDir, file))
|
|
38
|
+
)
|
|
39
|
+
|
|
36
40
|
if (!maxArgLength) {
|
|
37
41
|
debug('Skip chunking files because of undefined maxArgLength')
|
|
38
|
-
return [
|
|
42
|
+
return [normalizedFiles] // wrap in an array to return a single chunk
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
const normalizedFiles = files.map((file) =>
|
|
42
|
-
normalize(relative || !baseDir ? file : path.resolve(baseDir, file))
|
|
43
|
-
)
|
|
44
45
|
const fileListLength = normalizedFiles.join(' ').length
|
|
45
46
|
debug(
|
|
46
47
|
`Resolved an argument string length of ${fileListLength} characters from ${normalizedFiles.length} files`
|
|
47
48
|
)
|
|
48
49
|
const chunkCount = Math.min(Math.ceil(fileListLength / maxArgLength), normalizedFiles.length)
|
|
49
50
|
debug(`Creating ${chunkCount} chunks for maxArgLength of ${maxArgLength}`)
|
|
50
|
-
return chunkArray(
|
|
51
|
+
return chunkArray(normalizedFiles, chunkCount)
|
|
51
52
|
}
|
package/lib/execGit.js
CHANGED
|
@@ -9,7 +9,7 @@ module.exports = async function execGit(cmd, options = {}) {
|
|
|
9
9
|
const { stdout } = await execa('git', [].concat(cmd), {
|
|
10
10
|
...options,
|
|
11
11
|
all: true,
|
|
12
|
-
cwd: options.cwd || process.cwd()
|
|
12
|
+
cwd: options.cwd || process.cwd(),
|
|
13
13
|
})
|
|
14
14
|
return stdout
|
|
15
15
|
} catch ({ all }) {
|
package/lib/file.js
CHANGED
package/lib/generateTasks.js
CHANGED
|
@@ -21,7 +21,7 @@ module.exports = function generateTasks({
|
|
|
21
21
|
cwd = process.cwd(),
|
|
22
22
|
gitDir,
|
|
23
23
|
files,
|
|
24
|
-
relative = false
|
|
24
|
+
relative = false,
|
|
25
25
|
}) {
|
|
26
26
|
debug('Generating linter tasks')
|
|
27
27
|
|
|
@@ -46,7 +46,7 @@ module.exports = function generateTasks({
|
|
|
46
46
|
// If pattern doesn't look like a path, enable `matchBase` to
|
|
47
47
|
// match against filenames in every directory. This makes `*.js`
|
|
48
48
|
// match both `test.js` and `subdirectory/test.js`.
|
|
49
|
-
matchBase: !pattern.includes('/')
|
|
49
|
+
matchBase: !pattern.includes('/'),
|
|
50
50
|
}
|
|
51
51
|
).map((file) => normalize(relative ? file : path.resolve(cwd, file)))
|
|
52
52
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const getRenderer = ({ debug, quiet }, env = process.env) => {
|
|
4
|
+
if (quiet) return 'silent'
|
|
5
|
+
// Better support for dumb terminals: https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals
|
|
6
|
+
const isDumbTerminal = env.TERM === 'dumb'
|
|
7
|
+
if (isDumbTerminal || env.NODE_ENV === 'test') return 'test'
|
|
8
|
+
if (debug) return 'verbose'
|
|
9
|
+
return 'update'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = getRenderer
|
package/lib/gitWorkflow.js
CHANGED
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
const debug = require('debug')('lint-staged:git')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
|
|
6
|
-
const chunkFiles = require('./chunkFiles')
|
|
7
6
|
const execGit = require('./execGit')
|
|
8
7
|
const { readFile, unlink, writeFile } = require('./file')
|
|
8
|
+
const {
|
|
9
|
+
GitError,
|
|
10
|
+
RestoreOriginalStateError,
|
|
11
|
+
ApplyEmptyCommitError,
|
|
12
|
+
GetBackupStashError,
|
|
13
|
+
HideUnstagedChangesError,
|
|
14
|
+
RestoreMergeStatusError,
|
|
15
|
+
RestoreUnstagedChangesError,
|
|
16
|
+
} = require('./symbols')
|
|
9
17
|
|
|
10
18
|
const MERGE_HEAD = 'MERGE_HEAD'
|
|
11
19
|
const MERGE_MODE = 'MERGE_MODE'
|
|
@@ -43,25 +51,25 @@ const GIT_DIFF_ARGS = [
|
|
|
43
51
|
'--no-ext-diff', // disable external diff tools for consistent behaviour
|
|
44
52
|
'--src-prefix=a/', // force prefix for consistent behaviour
|
|
45
53
|
'--dst-prefix=b/', // force prefix for consistent behaviour
|
|
46
|
-
'--patch' // output a patch that can be applied
|
|
54
|
+
'--patch', // output a patch that can be applied
|
|
47
55
|
]
|
|
48
56
|
const GIT_APPLY_ARGS = ['-v', '--whitespace=nowarn', '--recount', '--unidiff-zero']
|
|
49
57
|
|
|
50
|
-
const handleError = (error, ctx) => {
|
|
51
|
-
ctx.
|
|
58
|
+
const handleError = (error, ctx, symbol) => {
|
|
59
|
+
ctx.errors.add(GitError)
|
|
60
|
+
if (symbol) ctx.errors.add(symbol)
|
|
52
61
|
throw error
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
class GitWorkflow {
|
|
56
|
-
constructor({ allowEmpty, gitConfigDir, gitDir,
|
|
65
|
+
constructor({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks }) {
|
|
57
66
|
this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: gitDir })
|
|
58
67
|
this.deletedFiles = []
|
|
59
68
|
this.gitConfigDir = gitConfigDir
|
|
60
69
|
this.gitDir = gitDir
|
|
61
70
|
this.unstagedDiff = null
|
|
62
71
|
this.allowEmpty = allowEmpty
|
|
63
|
-
this.
|
|
64
|
-
this.maxArgLength = maxArgLength
|
|
72
|
+
this.matchedFileChunks = matchedFileChunks
|
|
65
73
|
|
|
66
74
|
/**
|
|
67
75
|
* These three files hold state about an ongoing git merge
|
|
@@ -87,7 +95,7 @@ class GitWorkflow {
|
|
|
87
95
|
const stashes = await this.execGit(['stash', 'list'])
|
|
88
96
|
const index = stashes.split('\n').findIndex((line) => line.includes(STASH))
|
|
89
97
|
if (index === -1) {
|
|
90
|
-
ctx.
|
|
98
|
+
ctx.errors.add(GetBackupStashError)
|
|
91
99
|
throw new Error('lint-staged automatic backup is missing!')
|
|
92
100
|
}
|
|
93
101
|
return `stash@{${index}}`
|
|
@@ -115,7 +123,7 @@ class GitWorkflow {
|
|
|
115
123
|
await Promise.all([
|
|
116
124
|
readFile(this.mergeHeadFilename).then((buffer) => (this.mergeHeadBuffer = buffer)),
|
|
117
125
|
readFile(this.mergeModeFilename).then((buffer) => (this.mergeModeBuffer = buffer)),
|
|
118
|
-
readFile(this.mergeMsgFilename).then((buffer) => (this.mergeMsgBuffer = buffer))
|
|
126
|
+
readFile(this.mergeMsgFilename).then((buffer) => (this.mergeMsgBuffer = buffer)),
|
|
119
127
|
])
|
|
120
128
|
debug('Done backing up merge state!')
|
|
121
129
|
}
|
|
@@ -123,19 +131,23 @@ class GitWorkflow {
|
|
|
123
131
|
/**
|
|
124
132
|
* Restore meta information about ongoing git merge
|
|
125
133
|
*/
|
|
126
|
-
async restoreMergeStatus() {
|
|
134
|
+
async restoreMergeStatus(ctx) {
|
|
127
135
|
debug('Restoring merge state...')
|
|
128
136
|
try {
|
|
129
137
|
await Promise.all([
|
|
130
138
|
this.mergeHeadBuffer && writeFile(this.mergeHeadFilename, this.mergeHeadBuffer),
|
|
131
139
|
this.mergeModeBuffer && writeFile(this.mergeModeFilename, this.mergeModeBuffer),
|
|
132
|
-
this.mergeMsgBuffer && writeFile(this.mergeMsgFilename, this.mergeMsgBuffer)
|
|
140
|
+
this.mergeMsgBuffer && writeFile(this.mergeMsgFilename, this.mergeMsgBuffer),
|
|
133
141
|
])
|
|
134
142
|
debug('Done restoring merge state!')
|
|
135
143
|
} catch (error) {
|
|
136
144
|
debug('Failed restoring merge state with error:')
|
|
137
145
|
debug(error)
|
|
138
|
-
|
|
146
|
+
handleError(
|
|
147
|
+
new Error('Merge state could not be restored due to an error!'),
|
|
148
|
+
ctx,
|
|
149
|
+
RestoreMergeStatusError
|
|
150
|
+
)
|
|
139
151
|
}
|
|
140
152
|
}
|
|
141
153
|
|
|
@@ -159,6 +171,7 @@ class GitWorkflow {
|
|
|
159
171
|
return index !== ' ' && workingTree !== ' ' && index !== '?' && workingTree !== '?'
|
|
160
172
|
})
|
|
161
173
|
.map((line) => line.substr(3)) // Remove first three letters (index, workingTree, and a whitespace)
|
|
174
|
+
.filter(Boolean) // Filter empty string
|
|
162
175
|
debug('Found partially staged files:', partiallyStaged)
|
|
163
176
|
return partiallyStaged.length ? partiallyStaged : null
|
|
164
177
|
}
|
|
@@ -166,7 +179,7 @@ class GitWorkflow {
|
|
|
166
179
|
/**
|
|
167
180
|
* Create a diff of partially staged files and backup stash if enabled.
|
|
168
181
|
*/
|
|
169
|
-
async prepare(ctx
|
|
182
|
+
async prepare(ctx) {
|
|
170
183
|
try {
|
|
171
184
|
debug('Backing up original state...')
|
|
172
185
|
|
|
@@ -179,31 +192,28 @@ class GitWorkflow {
|
|
|
179
192
|
const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
|
|
180
193
|
const files = processRenames(this.partiallyStagedFiles)
|
|
181
194
|
await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
|
|
195
|
+
} else {
|
|
196
|
+
ctx.hasPartiallyStagedFiles = false
|
|
182
197
|
}
|
|
183
198
|
|
|
184
199
|
/**
|
|
185
200
|
* If backup stash should be skipped, no need to continue
|
|
186
201
|
*/
|
|
187
|
-
if (!shouldBackup) return
|
|
202
|
+
if (!ctx.shouldBackup) return
|
|
203
|
+
|
|
204
|
+
// When backup is enabled, the revert will clear ongoing merge status.
|
|
205
|
+
await this.backupMergeStatus()
|
|
188
206
|
|
|
189
207
|
// Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
|
|
190
|
-
// - in git versions =< 2.13.0 the
|
|
208
|
+
// - in git versions =< 2.13.0 the `git stash --keep-index` option resurrects deleted files
|
|
191
209
|
// - git stash can't infer RD or MD states correctly, and will lose the deletion
|
|
192
210
|
this.deletedFiles = await this.getDeletedFiles()
|
|
193
211
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
await this.execGit(['stash', 'save', STASH])
|
|
200
|
-
await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash()])
|
|
201
|
-
|
|
202
|
-
// Restore meta information about ongoing git merge, cleared by `git stash`
|
|
203
|
-
await this.restoreMergeStatus()
|
|
204
|
-
|
|
205
|
-
// If stashing resurrected deleted files, clean them out
|
|
206
|
-
await Promise.all(this.deletedFiles.map((file) => unlink(file)))
|
|
212
|
+
// Save stash of all staged files.
|
|
213
|
+
// The `stash create` command creates a dangling commit without removing any files,
|
|
214
|
+
// and `stash store` saves it as an actual stash.
|
|
215
|
+
const hash = await this.execGit(['stash', 'create'])
|
|
216
|
+
await this.execGit(['stash', 'store', '--quiet', '--message', STASH, hash])
|
|
207
217
|
|
|
208
218
|
debug('Done backing up original state!')
|
|
209
219
|
} catch (error) {
|
|
@@ -223,8 +233,7 @@ class GitWorkflow {
|
|
|
223
233
|
* `git checkout --force` doesn't throw errors, so it shouldn't be possible to get here.
|
|
224
234
|
* If this does fail, the handleError method will set ctx.gitError and lint-staged will fail.
|
|
225
235
|
*/
|
|
226
|
-
ctx
|
|
227
|
-
handleError(error, ctx)
|
|
236
|
+
handleError(error, ctx, HideUnstagedChangesError)
|
|
228
237
|
}
|
|
229
238
|
}
|
|
230
239
|
|
|
@@ -234,19 +243,12 @@ class GitWorkflow {
|
|
|
234
243
|
*/
|
|
235
244
|
async applyModifications(ctx) {
|
|
236
245
|
debug('Adding task modifications to index...')
|
|
237
|
-
// `matchedFiles` includes staged files that lint-staged originally detected and matched against a task.
|
|
238
|
-
// Add only these files so any 3rd-party edits to other files won't be included in the commit.
|
|
239
|
-
const files = Array.from(this.matchedFiles)
|
|
240
|
-
// Chunk files for better Windows compatibility
|
|
241
|
-
const matchedFileChunks = chunkFiles({
|
|
242
|
-
baseDir: this.gitDir,
|
|
243
|
-
files,
|
|
244
|
-
maxArgLength: this.maxArgLength
|
|
245
|
-
})
|
|
246
246
|
|
|
247
|
+
// `matchedFileChunks` includes staged files that lint-staged originally detected and matched against a task.
|
|
248
|
+
// Add only these files so any 3rd-party edits to other files won't be included in the commit.
|
|
247
249
|
// These additions per chunk are run "serially" to prevent race conditions.
|
|
248
250
|
// Git add creates a lockfile in the repo causing concurrent operations to fail.
|
|
249
|
-
for (const files of matchedFileChunks) {
|
|
251
|
+
for (const files of this.matchedFileChunks) {
|
|
250
252
|
await this.execGit(['add', '--', ...files])
|
|
251
253
|
}
|
|
252
254
|
|
|
@@ -256,8 +258,7 @@ class GitWorkflow {
|
|
|
256
258
|
if (!stagedFilesAfterAdd && !this.allowEmpty) {
|
|
257
259
|
// Tasks reverted all staged changes and the commit would be empty
|
|
258
260
|
// Throw error to stop commit unless `--allow-empty` was used
|
|
259
|
-
ctx
|
|
260
|
-
handleError(new Error('Prevented an empty git commit!'), ctx)
|
|
261
|
+
handleError(new Error('Prevented an empty git commit!'), ctx, ApplyEmptyCommitError)
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
264
|
|
|
@@ -281,10 +282,10 @@ class GitWorkflow {
|
|
|
281
282
|
} catch (threeWayApplyError) {
|
|
282
283
|
debug('Error while restoring unstaged changes using 3-way merge:')
|
|
283
284
|
debug(threeWayApplyError)
|
|
284
|
-
ctx.gitRestoreUnstagedChangesError = true
|
|
285
285
|
handleError(
|
|
286
286
|
new Error('Unstaged changes could not be restored due to a merge conflict!'),
|
|
287
|
-
ctx
|
|
287
|
+
ctx,
|
|
288
|
+
RestoreUnstagedChangesError
|
|
288
289
|
)
|
|
289
290
|
}
|
|
290
291
|
}
|
|
@@ -300,7 +301,7 @@ class GitWorkflow {
|
|
|
300
301
|
await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash(ctx)])
|
|
301
302
|
|
|
302
303
|
// Restore meta information about ongoing git merge
|
|
303
|
-
await this.restoreMergeStatus()
|
|
304
|
+
await this.restoreMergeStatus(ctx)
|
|
304
305
|
|
|
305
306
|
// If stashing resurrected deleted files, clean them out
|
|
306
307
|
await Promise.all(this.deletedFiles.map((file) => unlink(file)))
|
|
@@ -310,8 +311,7 @@ class GitWorkflow {
|
|
|
310
311
|
|
|
311
312
|
debug('Done restoring original state!')
|
|
312
313
|
} catch (error) {
|
|
313
|
-
ctx
|
|
314
|
-
handleError(error, ctx)
|
|
314
|
+
handleError(error, ctx, RestoreOriginalStateError)
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
|
package/lib/index.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const dedent = require('dedent')
|
|
4
4
|
const { cosmiconfig } = require('cosmiconfig')
|
|
5
|
+
const debugLog = require('debug')('lint-staged')
|
|
5
6
|
const stringifyObject = require('stringify-object')
|
|
6
|
-
|
|
7
|
+
|
|
8
|
+
const { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } = require('./messages')
|
|
9
|
+
const printTaskOutput = require('./printTaskOutput')
|
|
7
10
|
const runAll = require('./runAll')
|
|
11
|
+
const { ApplyEmptyCommitError, GetBackupStashError, GitError } = require('./symbols')
|
|
8
12
|
const validateConfig = require('./validateConfig')
|
|
9
13
|
|
|
10
|
-
const debugLog = require('debug')('lint-staged')
|
|
11
|
-
|
|
12
14
|
const errConfigNotFound = new Error('Config could not be found')
|
|
13
15
|
|
|
14
16
|
function resolveConfig(configPath) {
|
|
@@ -28,8 +30,8 @@ function loadConfig(configPath) {
|
|
|
28
30
|
'.lintstagedrc.yaml',
|
|
29
31
|
'.lintstagedrc.yml',
|
|
30
32
|
'.lintstagedrc.js',
|
|
31
|
-
'lint-staged.config.js'
|
|
32
|
-
]
|
|
33
|
+
'lint-staged.config.js',
|
|
34
|
+
],
|
|
33
35
|
})
|
|
34
36
|
|
|
35
37
|
return configPath ? explorer.load(resolveConfig(configPath)) : explorer.search()
|
|
@@ -46,12 +48,14 @@ function loadConfig(configPath) {
|
|
|
46
48
|
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
|
|
47
49
|
* @param {object} [options.config] - Object with configuration for programmatic API
|
|
48
50
|
* @param {string} [options.configPath] - Path to configuration file
|
|
51
|
+
* @param {Object} [options.cwd] - Current working directory
|
|
49
52
|
* @param {boolean} [options.debug] - Enable debug mode
|
|
50
53
|
* @param {number} [options.maxArgLength] - Maximum argument string length
|
|
51
54
|
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
|
|
52
55
|
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
|
|
53
56
|
* @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
|
|
54
57
|
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
|
|
58
|
+
* @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
|
|
55
59
|
* @param {Logger} [logger]
|
|
56
60
|
*
|
|
57
61
|
* @returns {Promise<boolean>} Promise of whether the linting passed or failed
|
|
@@ -62,12 +66,14 @@ module.exports = async function lintStaged(
|
|
|
62
66
|
concurrent = true,
|
|
63
67
|
config: configObject,
|
|
64
68
|
configPath,
|
|
69
|
+
cwd = process.cwd(),
|
|
65
70
|
debug = false,
|
|
66
71
|
maxArgLength,
|
|
67
72
|
quiet = false,
|
|
68
73
|
relative = false,
|
|
69
74
|
shell = false,
|
|
70
|
-
stash = true
|
|
75
|
+
stash = true,
|
|
76
|
+
verbose = false,
|
|
71
77
|
} = {},
|
|
72
78
|
logger = console
|
|
73
79
|
) {
|
|
@@ -98,14 +104,38 @@ module.exports = async function lintStaged(
|
|
|
98
104
|
delete process.env.GIT_LITERAL_PATHSPECS
|
|
99
105
|
|
|
100
106
|
try {
|
|
101
|
-
await runAll(
|
|
102
|
-
{
|
|
107
|
+
const ctx = await runAll(
|
|
108
|
+
{
|
|
109
|
+
allowEmpty,
|
|
110
|
+
concurrent,
|
|
111
|
+
config,
|
|
112
|
+
cwd,
|
|
113
|
+
debug,
|
|
114
|
+
maxArgLength,
|
|
115
|
+
quiet,
|
|
116
|
+
relative,
|
|
117
|
+
shell,
|
|
118
|
+
stash,
|
|
119
|
+
verbose,
|
|
120
|
+
},
|
|
103
121
|
logger
|
|
104
122
|
)
|
|
105
|
-
debugLog('
|
|
123
|
+
debugLog('Tasks were executed successfully!')
|
|
124
|
+
printTaskOutput(ctx, logger)
|
|
106
125
|
return true
|
|
107
126
|
} catch (runAllError) {
|
|
108
|
-
|
|
127
|
+
const { ctx } = runAllError
|
|
128
|
+
if (ctx.errors.has(ApplyEmptyCommitError)) {
|
|
129
|
+
logger.warn(PREVENTED_EMPTY_COMMIT)
|
|
130
|
+
} else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
|
|
131
|
+
logger.error(GIT_ERROR)
|
|
132
|
+
if (ctx.shouldBackup) {
|
|
133
|
+
// No sense to show this if the backup stash itself is missing.
|
|
134
|
+
logger.error(RESTORE_STASH_EXAMPLE)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
printTaskOutput(ctx, logger)
|
|
109
139
|
return false
|
|
110
140
|
}
|
|
111
141
|
} catch (lintStagedError) {
|
package/lib/makeCmdTasks.js
CHANGED
|
@@ -13,8 +13,9 @@ const debug = require('debug')('lint-staged:make-cmd-tasks')
|
|
|
13
13
|
* @param {Array<string>} options.files
|
|
14
14
|
* @param {string} options.gitDir
|
|
15
15
|
* @param {Boolean} shell
|
|
16
|
+
* @param {Boolean} verbose
|
|
16
17
|
*/
|
|
17
|
-
module.exports = async function makeCmdTasks({ commands, files, gitDir, shell }) {
|
|
18
|
+
module.exports = async function makeCmdTasks({ commands, files, gitDir, shell, verbose }) {
|
|
18
19
|
debug('Creating listr tasks for commands %o', commands)
|
|
19
20
|
const commandArray = Array.isArray(commands) ? commands : [commands]
|
|
20
21
|
const cmdTasks = []
|
|
@@ -49,7 +50,7 @@ module.exports = async function makeCmdTasks({ commands, files, gitDir, shell })
|
|
|
49
50
|
cmdTasks.push({
|
|
50
51
|
title,
|
|
51
52
|
command,
|
|
52
|
-
task: resolveTaskFn({ command, files, gitDir, isFn, shell })
|
|
53
|
+
task: resolveTaskFn({ command, files, gitDir, isFn, shell, verbose }),
|
|
53
54
|
})
|
|
54
55
|
}
|
|
55
56
|
}
|
package/lib/messages.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk')
|
|
4
|
+
const { error, info, warning } = require('log-symbols')
|
|
5
|
+
|
|
6
|
+
const NOT_GIT_REPO = chalk.redBright(`${error} Current directory is not a git directory!`)
|
|
7
|
+
|
|
8
|
+
const FAILED_GET_STAGED_FILES = chalk.redBright(`${error} Failed to get staged files!`)
|
|
9
|
+
|
|
10
|
+
const NO_STAGED_FILES = `${info} No staged files found.`
|
|
11
|
+
|
|
12
|
+
const NO_TASKS = `${info} No staged files match any configured task.`
|
|
13
|
+
|
|
14
|
+
const skippingBackup = (hasInitialCommit) => {
|
|
15
|
+
const reason = hasInitialCommit ? '`--no-stash` was used' : 'there’s no initial commit yet'
|
|
16
|
+
return `${warning} ${chalk.yellow(`Skipping backup because ${reason}.\n`)}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEPRECATED_GIT_ADD = `${warning} ${chalk.yellow(
|
|
20
|
+
`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.`
|
|
21
|
+
)}
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
const TASK_ERROR = 'Skipped because of errors from tasks.'
|
|
25
|
+
|
|
26
|
+
const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.'
|
|
27
|
+
|
|
28
|
+
const GIT_ERROR = `\n ${error} ${chalk.red(`lint-staged failed due to a git error.`)}`
|
|
29
|
+
|
|
30
|
+
const PREVENTED_EMPTY_COMMIT = `
|
|
31
|
+
${warning} ${chalk.yellow(`lint-staged prevented an empty git commit.
|
|
32
|
+
Use the --allow-empty option to continue, or check your task configuration`)}
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
const RESTORE_STASH_EXAMPLE = ` Any lost modifications can be restored from a git stash:
|
|
36
|
+
|
|
37
|
+
> git stash list
|
|
38
|
+
stash@{0}: automatic lint-staged backup
|
|
39
|
+
> git stash apply --index stash@{0}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
NOT_GIT_REPO,
|
|
44
|
+
FAILED_GET_STAGED_FILES,
|
|
45
|
+
NO_STAGED_FILES,
|
|
46
|
+
NO_TASKS,
|
|
47
|
+
skippingBackup,
|
|
48
|
+
DEPRECATED_GIT_ADD,
|
|
49
|
+
TASK_ERROR,
|
|
50
|
+
SKIPPED_GIT_ERROR,
|
|
51
|
+
GIT_ERROR,
|
|
52
|
+
PREVENTED_EMPTY_COMMIT,
|
|
53
|
+
RESTORE_STASH_EXAMPLE,
|
|
54
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle logging of listr `ctx.output` to the specified `logger`
|
|
5
|
+
* @param {Object} ctx - The listr initial state
|
|
6
|
+
* @param {Object} logger - The logger
|
|
7
|
+
*/
|
|
8
|
+
const printTaskOutput = (ctx = {}, logger) => {
|
|
9
|
+
if (!Array.isArray(ctx.output)) return
|
|
10
|
+
const log = ctx.errors && ctx.errors.size > 0 ? logger.error : logger.log
|
|
11
|
+
for (const line of ctx.output) {
|
|
12
|
+
log(line)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = printTaskOutput
|
package/lib/resolveTaskFn.js
CHANGED
|
@@ -1,57 +1,66 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const { parseArgsStringToArgv } = require('string-argv')
|
|
5
|
-
const dedent = require('dedent')
|
|
3
|
+
const { redBright, dim } = require('chalk')
|
|
6
4
|
const execa = require('execa')
|
|
7
|
-
const symbols = require('log-symbols')
|
|
8
|
-
|
|
9
5
|
const debug = require('debug')('lint-staged:task')
|
|
6
|
+
const { parseArgsStringToArgv } = require('string-argv')
|
|
7
|
+
const { error, info } = require('log-symbols')
|
|
8
|
+
|
|
9
|
+
const { getInitialState } = require('./state')
|
|
10
|
+
const { TaskError } = require('./symbols')
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const getTag = ({ code, killed, signal }) => signal || (killed && 'KILLED') || code || 'FAILED'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
* If we set the message on the error instance, it gets logged multiple times(see #142).
|
|
16
|
-
* So we set the actual error message in a private field and extract it later,
|
|
17
|
-
* log only once.
|
|
15
|
+
* Handle task console output.
|
|
18
16
|
*
|
|
19
|
-
* @param {string}
|
|
17
|
+
* @param {string} command
|
|
18
|
+
* @param {Object} result
|
|
19
|
+
* @param {string} result.stdout
|
|
20
|
+
* @param {string} result.stderr
|
|
21
|
+
* @param {boolean} result.failed
|
|
22
|
+
* @param {boolean} result.killed
|
|
23
|
+
* @param {string} result.signal
|
|
24
|
+
* @param {Object} ctx
|
|
20
25
|
* @returns {Error}
|
|
21
26
|
*/
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const handleOutput = (command, result, ctx, isError = false) => {
|
|
28
|
+
const { stderr, stdout } = result
|
|
29
|
+
const hasOutput = !!stderr || !!stdout
|
|
30
|
+
|
|
31
|
+
if (hasOutput) {
|
|
32
|
+
const outputTitle = isError ? redBright(`${error} ${command}:`) : `${info} ${command}:`
|
|
33
|
+
const output = []
|
|
34
|
+
.concat(ctx.quiet ? [] : ['', outputTitle])
|
|
35
|
+
.concat(stderr ? stderr : [])
|
|
36
|
+
.concat(stdout ? stdout : [])
|
|
37
|
+
ctx.output.push(output.join('\n'))
|
|
38
|
+
} else if (isError) {
|
|
39
|
+
// Show generic error when task had no output
|
|
40
|
+
const tag = getTag(result)
|
|
41
|
+
const message = redBright(`\n${error} ${command} failed without output (${tag}).`)
|
|
42
|
+
if (!ctx.quiet) ctx.output.push(message)
|
|
43
|
+
}
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/**
|
|
29
|
-
* Create a
|
|
47
|
+
* Create a error output dependding on process result.
|
|
30
48
|
*
|
|
31
|
-
* @param {string}
|
|
49
|
+
* @param {string} command
|
|
32
50
|
* @param {Object} result
|
|
33
51
|
* @param {string} result.stdout
|
|
34
52
|
* @param {string} result.stderr
|
|
35
53
|
* @param {boolean} result.failed
|
|
36
54
|
* @param {boolean} result.killed
|
|
37
55
|
* @param {string} result.signal
|
|
38
|
-
* @param {Object}
|
|
56
|
+
* @param {Object} ctx
|
|
39
57
|
* @returns {Error}
|
|
40
58
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
`${symbols.warning} ${chalk.yellow(`${linter} was terminated with ${signal}`)}`
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
return throwError(dedent`${symbols.error} ${chalk.redBright(
|
|
50
|
-
`${linter} found some errors. Please fix them and try committing again.`
|
|
51
|
-
)}
|
|
52
|
-
${stdout}
|
|
53
|
-
${stderr}
|
|
54
|
-
`)
|
|
59
|
+
const makeErr = (command, result, ctx) => {
|
|
60
|
+
ctx.errors.add(TaskError)
|
|
61
|
+
handleOutput(command, result, ctx, true)
|
|
62
|
+
const tag = getTag(result)
|
|
63
|
+
return new Error(`${redBright(command)} ${dim(`[${tag}]`)}`)
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -64,9 +73,18 @@ function makeErr(linter, result, context = {}) {
|
|
|
64
73
|
* @param {Array<string>} options.files — Filepaths to run the linter task against
|
|
65
74
|
* @param {Boolean} [options.relative] — Whether the filepaths should be relative
|
|
66
75
|
* @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
|
|
76
|
+
* @param {Boolean} [options.verbose] — Always show task verbose
|
|
67
77
|
* @returns {function(): Promise<Array<string>>}
|
|
68
78
|
*/
|
|
69
|
-
module.exports = function resolveTaskFn({
|
|
79
|
+
module.exports = function resolveTaskFn({
|
|
80
|
+
command,
|
|
81
|
+
files,
|
|
82
|
+
gitDir,
|
|
83
|
+
isFn,
|
|
84
|
+
relative,
|
|
85
|
+
shell = false,
|
|
86
|
+
verbose = false,
|
|
87
|
+
}) {
|
|
70
88
|
const [cmd, ...args] = parseArgsStringToArgv(command)
|
|
71
89
|
debug('cmd:', cmd)
|
|
72
90
|
debug('args:', args)
|
|
@@ -81,16 +99,17 @@ module.exports = function resolveTaskFn({ command, files, gitDir, isFn, relative
|
|
|
81
99
|
}
|
|
82
100
|
debug('execaOptions:', execaOptions)
|
|
83
101
|
|
|
84
|
-
return async (ctx) => {
|
|
85
|
-
const
|
|
102
|
+
return async (ctx = getInitialState()) => {
|
|
103
|
+
const result = await (shell
|
|
86
104
|
? execa.command(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
|
|
87
|
-
: execa(cmd, isFn ? args : args.concat(files), execaOptions)
|
|
88
|
-
const result = await promise
|
|
105
|
+
: execa(cmd, isFn ? args : args.concat(files), execaOptions))
|
|
89
106
|
|
|
90
107
|
if (result.failed || result.killed || result.signal != null) {
|
|
91
|
-
throw makeErr(
|
|
108
|
+
throw makeErr(command, result, ctx)
|
|
92
109
|
}
|
|
93
110
|
|
|
94
|
-
|
|
111
|
+
if (verbose) {
|
|
112
|
+
handleOutput(command, result, ctx)
|
|
113
|
+
}
|
|
95
114
|
}
|
|
96
115
|
}
|
package/lib/runAll.js
CHANGED
|
@@ -2,61 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('./index').Logger} Logger */
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const Listr = require('listr')
|
|
7
|
-
const symbols = require('log-symbols')
|
|
5
|
+
const { Listr } = require('listr2')
|
|
8
6
|
|
|
9
7
|
const chunkFiles = require('./chunkFiles')
|
|
8
|
+
const debugLog = require('debug')('lint-staged:run')
|
|
10
9
|
const execGit = require('./execGit')
|
|
11
10
|
const generateTasks = require('./generateTasks')
|
|
11
|
+
const getRenderer = require('./getRenderer')
|
|
12
12
|
const getStagedFiles = require('./getStagedFiles')
|
|
13
13
|
const GitWorkflow = require('./gitWorkflow')
|
|
14
14
|
const makeCmdTasks = require('./makeCmdTasks')
|
|
15
|
+
const {
|
|
16
|
+
DEPRECATED_GIT_ADD,
|
|
17
|
+
FAILED_GET_STAGED_FILES,
|
|
18
|
+
NOT_GIT_REPO,
|
|
19
|
+
NO_STAGED_FILES,
|
|
20
|
+
NO_TASKS,
|
|
21
|
+
SKIPPED_GIT_ERROR,
|
|
22
|
+
skippingBackup,
|
|
23
|
+
} = require('./messages')
|
|
15
24
|
const resolveGitRepo = require('./resolveGitRepo')
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
GIT_ERROR: 'Skipped because of previous git error.'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const shouldSkipApplyModifications = (ctx) => {
|
|
33
|
-
// Should be skipped in case of git errors
|
|
34
|
-
if (ctx.gitError) {
|
|
35
|
-
return MESSAGES.GIT_ERROR
|
|
36
|
-
}
|
|
37
|
-
// Should be skipped when tasks fail
|
|
38
|
-
if (ctx.taskError) {
|
|
39
|
-
return MESSAGES.TASK_ERROR
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const shouldSkipRevert = (ctx) => {
|
|
44
|
-
// Should be skipped in case of unknown git errors
|
|
45
|
-
if (ctx.gitError && !ctx.gitApplyEmptyCommitError && !ctx.gitRestoreUnstagedChangesError) {
|
|
46
|
-
return MESSAGES.GIT_ERROR
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const shouldSkipCleanup = (ctx) => {
|
|
51
|
-
// Should be skipped in case of unknown git errors
|
|
52
|
-
if (ctx.gitError && !ctx.gitApplyEmptyCommitError && !ctx.gitRestoreUnstagedChangesError) {
|
|
53
|
-
return MESSAGES.GIT_ERROR
|
|
54
|
-
}
|
|
55
|
-
// Should be skipped when reverting to original state fails
|
|
56
|
-
if (ctx.gitRestoreOriginalStateError) {
|
|
57
|
-
return MESSAGES.GIT_ERROR
|
|
58
|
-
}
|
|
59
|
-
}
|
|
25
|
+
const {
|
|
26
|
+
applyModificationsSkipped,
|
|
27
|
+
cleanupEnabled,
|
|
28
|
+
cleanupSkipped,
|
|
29
|
+
getInitialState,
|
|
30
|
+
hasPartiallyStagedFiles,
|
|
31
|
+
restoreOriginalStateEnabled,
|
|
32
|
+
restoreOriginalStateSkipped,
|
|
33
|
+
restoreUnstagedChangesSkipped,
|
|
34
|
+
} = require('./state')
|
|
35
|
+
const { GitRepoError, GetStagedFilesError, GitError } = require('./symbols')
|
|
36
|
+
|
|
37
|
+
const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ctx })
|
|
60
38
|
|
|
61
39
|
/**
|
|
62
40
|
* Executes all tasks and either resolves or rejects the promise
|
|
@@ -72,6 +50,7 @@ const shouldSkipCleanup = (ctx) => {
|
|
|
72
50
|
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
|
|
73
51
|
* @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
|
|
74
52
|
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
|
|
53
|
+
* @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
|
|
75
54
|
* @param {Logger} logger
|
|
76
55
|
* @returns {Promise}
|
|
77
56
|
*/
|
|
@@ -86,14 +65,21 @@ const runAll = async (
|
|
|
86
65
|
quiet = false,
|
|
87
66
|
relative = false,
|
|
88
67
|
shell = false,
|
|
89
|
-
stash = true
|
|
68
|
+
stash = true,
|
|
69
|
+
verbose = false,
|
|
90
70
|
},
|
|
91
71
|
logger = console
|
|
92
72
|
) => {
|
|
93
73
|
debugLog('Running all linter scripts')
|
|
94
74
|
|
|
75
|
+
const ctx = getInitialState({ quiet })
|
|
76
|
+
|
|
95
77
|
const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
|
|
96
|
-
if (!gitDir)
|
|
78
|
+
if (!gitDir) {
|
|
79
|
+
if (!quiet) ctx.output.push(NOT_GIT_REPO)
|
|
80
|
+
ctx.errors.add(GitRepoError)
|
|
81
|
+
throw createError(ctx)
|
|
82
|
+
}
|
|
97
83
|
|
|
98
84
|
// Test whether we have any commits or not.
|
|
99
85
|
// Stashing must be disabled with no initial commit.
|
|
@@ -102,19 +88,23 @@ const runAll = async (
|
|
|
102
88
|
.catch(() => false)
|
|
103
89
|
|
|
104
90
|
// Lint-staged should create a backup stash only when there's an initial commit
|
|
105
|
-
|
|
106
|
-
if (!shouldBackup) {
|
|
107
|
-
|
|
108
|
-
logger.warn(`${symbols.warning} ${chalk.yellow(`Skipping backup because ${reason}.\n`)}`)
|
|
91
|
+
ctx.shouldBackup = hasInitialCommit && stash
|
|
92
|
+
if (!ctx.shouldBackup) {
|
|
93
|
+
logger.warn(skippingBackup(hasInitialCommit))
|
|
109
94
|
}
|
|
110
95
|
|
|
111
96
|
const files = await getStagedFiles({ cwd: gitDir })
|
|
112
|
-
if (!files)
|
|
97
|
+
if (!files) {
|
|
98
|
+
if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
|
|
99
|
+
ctx.errors.add(GetStagedFilesError)
|
|
100
|
+
throw createError(ctx, GetStagedFilesError)
|
|
101
|
+
}
|
|
113
102
|
debugLog('Loaded list of staged files in git:\n%O', files)
|
|
114
103
|
|
|
115
104
|
// If there are no files avoid executing any lint-staged logic
|
|
116
105
|
if (files.length === 0) {
|
|
117
|
-
|
|
106
|
+
if (!quiet) ctx.output.push(NO_STAGED_FILES)
|
|
107
|
+
return ctx
|
|
118
108
|
}
|
|
119
109
|
|
|
120
110
|
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
|
|
@@ -126,9 +116,10 @@ const runAll = async (
|
|
|
126
116
|
let hasDeprecatedGitAdd = false
|
|
127
117
|
|
|
128
118
|
const listrOptions = {
|
|
119
|
+
ctx,
|
|
129
120
|
dateFormat: false,
|
|
130
121
|
exitOnError: false,
|
|
131
|
-
renderer: getRenderer({ debug, quiet })
|
|
122
|
+
renderer: getRenderer({ debug, quiet }),
|
|
132
123
|
}
|
|
133
124
|
|
|
134
125
|
const listrTasks = []
|
|
@@ -145,7 +136,8 @@ const runAll = async (
|
|
|
145
136
|
commands: task.commands,
|
|
146
137
|
files: task.fileList,
|
|
147
138
|
gitDir,
|
|
148
|
-
shell
|
|
139
|
+
shell,
|
|
140
|
+
verbose,
|
|
149
141
|
})
|
|
150
142
|
|
|
151
143
|
// Add files from task to match set
|
|
@@ -161,9 +153,9 @@ const runAll = async (
|
|
|
161
153
|
new Listr(subTasks, {
|
|
162
154
|
// In sub-tasks we don't want to run concurrently
|
|
163
155
|
// and we want to abort on errors
|
|
164
|
-
|
|
156
|
+
...listrOptions,
|
|
165
157
|
concurrent: false,
|
|
166
|
-
exitOnError: true
|
|
158
|
+
exitOnError: true,
|
|
167
159
|
}),
|
|
168
160
|
skip: () => {
|
|
169
161
|
// Skip task when no files matched
|
|
@@ -171,7 +163,7 @@ const runAll = async (
|
|
|
171
163
|
return `No staged files match ${task.pattern}`
|
|
172
164
|
}
|
|
173
165
|
return false
|
|
174
|
-
}
|
|
166
|
+
},
|
|
175
167
|
})
|
|
176
168
|
}
|
|
177
169
|
|
|
@@ -180,103 +172,84 @@ const runAll = async (
|
|
|
180
172
|
title:
|
|
181
173
|
chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...',
|
|
182
174
|
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }),
|
|
183
|
-
skip: (
|
|
175
|
+
skip: () => {
|
|
184
176
|
// Skip if the first step (backup) failed
|
|
185
|
-
if (ctx.
|
|
177
|
+
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
186
178
|
// Skip chunk when no every task is skipped (due to no matches)
|
|
187
179
|
if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.'
|
|
188
180
|
return false
|
|
189
|
-
}
|
|
181
|
+
},
|
|
190
182
|
})
|
|
191
183
|
}
|
|
192
184
|
|
|
193
185
|
if (hasDeprecatedGitAdd) {
|
|
194
|
-
logger.warn(
|
|
195
|
-
`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.`
|
|
196
|
-
)}
|
|
197
|
-
`)
|
|
186
|
+
logger.warn(DEPRECATED_GIT_ADD)
|
|
198
187
|
}
|
|
199
188
|
|
|
200
189
|
// If all of the configured tasks should be skipped
|
|
201
190
|
// avoid executing any lint-staged logic
|
|
202
191
|
if (listrTasks.every((task) => task.skip())) {
|
|
203
|
-
|
|
204
|
-
return
|
|
192
|
+
if (!quiet) ctx.output.push(NO_TASKS)
|
|
193
|
+
return ctx
|
|
205
194
|
}
|
|
206
195
|
|
|
207
|
-
|
|
196
|
+
// Chunk matched files for better Windows compatibility
|
|
197
|
+
const matchedFileChunks = chunkFiles({
|
|
198
|
+
// matched files are relative to `cwd`, not `gitDir`, when `relative` is used
|
|
199
|
+
baseDir: cwd,
|
|
200
|
+
files: Array.from(matchedFiles),
|
|
201
|
+
maxArgLength,
|
|
202
|
+
relative: false,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks })
|
|
208
206
|
|
|
209
207
|
const runner = new Listr(
|
|
210
208
|
[
|
|
211
209
|
{
|
|
212
210
|
title: 'Preparing...',
|
|
213
|
-
task: (ctx) => git.prepare(ctx,
|
|
211
|
+
task: (ctx) => git.prepare(ctx),
|
|
214
212
|
},
|
|
215
213
|
{
|
|
216
214
|
title: 'Hiding unstaged changes to partially staged files...',
|
|
217
215
|
task: (ctx) => git.hideUnstagedChanges(ctx),
|
|
218
|
-
enabled:
|
|
216
|
+
enabled: hasPartiallyStagedFiles,
|
|
219
217
|
},
|
|
220
218
|
...listrTasks,
|
|
221
219
|
{
|
|
222
220
|
title: 'Applying modifications...',
|
|
223
221
|
task: (ctx) => git.applyModifications(ctx),
|
|
224
|
-
|
|
225
|
-
skip: (ctx) => shouldBackup && shouldSkipApplyModifications(ctx)
|
|
222
|
+
skip: applyModificationsSkipped,
|
|
226
223
|
},
|
|
227
224
|
{
|
|
228
225
|
title: 'Restoring unstaged changes to partially staged files...',
|
|
229
226
|
task: (ctx) => git.restoreUnstagedChanges(ctx),
|
|
230
|
-
enabled:
|
|
231
|
-
skip:
|
|
227
|
+
enabled: hasPartiallyStagedFiles,
|
|
228
|
+
skip: restoreUnstagedChangesSkipped,
|
|
232
229
|
},
|
|
233
230
|
{
|
|
234
231
|
title: 'Reverting to original state because of errors...',
|
|
235
232
|
task: (ctx) => git.restoreOriginalState(ctx),
|
|
236
|
-
enabled:
|
|
237
|
-
|
|
238
|
-
(ctx.taskError || ctx.gitApplyEmptyCommitError || ctx.gitRestoreUnstagedChangesError),
|
|
239
|
-
skip: shouldSkipRevert
|
|
233
|
+
enabled: restoreOriginalStateEnabled,
|
|
234
|
+
skip: restoreOriginalStateSkipped,
|
|
240
235
|
},
|
|
241
236
|
{
|
|
242
237
|
title: 'Cleaning up...',
|
|
243
238
|
task: (ctx) => git.cleanup(ctx),
|
|
244
|
-
enabled:
|
|
245
|
-
skip:
|
|
246
|
-
}
|
|
239
|
+
enabled: cleanupEnabled,
|
|
240
|
+
skip: cleanupSkipped,
|
|
241
|
+
},
|
|
247
242
|
],
|
|
248
243
|
listrOptions
|
|
249
244
|
)
|
|
250
245
|
|
|
251
|
-
|
|
252
|
-
await runner.run({})
|
|
253
|
-
} catch (error) {
|
|
254
|
-
if (error.context.gitApplyEmptyCommitError) {
|
|
255
|
-
logger.warn(`
|
|
256
|
-
${symbols.warning} ${chalk.yellow(`lint-staged prevented an empty git commit.
|
|
257
|
-
Use the --allow-empty option to continue, or check your task configuration`)}
|
|
258
|
-
`)
|
|
259
|
-
} else if (error.context.gitError && !error.context.gitGetBackupStashError) {
|
|
260
|
-
logger.error(`\n ${symbols.error} ${chalk.red(`lint-staged failed due to a git error.`)}`)
|
|
261
|
-
|
|
262
|
-
if (shouldBackup) {
|
|
263
|
-
// No sense to show this if the backup stash itself is missing.
|
|
264
|
-
logger.error(` Any lost modifications can be restored from a git stash:
|
|
265
|
-
|
|
266
|
-
> git stash list
|
|
267
|
-
stash@{0}: On master: automatic lint-staged backup
|
|
268
|
-
> git stash apply --index stash@{0}\n`)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
246
|
+
await runner.run()
|
|
271
247
|
|
|
272
|
-
|
|
248
|
+
if (ctx.errors.size > 0) {
|
|
249
|
+
throw createError(ctx)
|
|
273
250
|
}
|
|
251
|
+
|
|
252
|
+
return ctx
|
|
274
253
|
}
|
|
275
254
|
|
|
276
255
|
module.exports = runAll
|
|
277
|
-
|
|
278
|
-
module.exports.shouldSkip = {
|
|
279
|
-
shouldSkipApplyModifications,
|
|
280
|
-
shouldSkipRevert,
|
|
281
|
-
shouldSkipCleanup
|
|
282
|
-
}
|
package/lib/state.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { GIT_ERROR, TASK_ERROR } = require('./messages')
|
|
4
|
+
const {
|
|
5
|
+
ApplyEmptyCommitError,
|
|
6
|
+
TaskError,
|
|
7
|
+
RestoreOriginalStateError,
|
|
8
|
+
GitError,
|
|
9
|
+
RestoreUnstagedChangesError,
|
|
10
|
+
} = require('./symbols')
|
|
11
|
+
|
|
12
|
+
const getInitialState = ({ quiet = false } = {}) => ({
|
|
13
|
+
hasPartiallyStagedFiles: null,
|
|
14
|
+
shouldBackup: null,
|
|
15
|
+
errors: new Set([]),
|
|
16
|
+
output: [],
|
|
17
|
+
quiet,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const hasPartiallyStagedFiles = (ctx) => ctx.hasPartiallyStagedFiles
|
|
21
|
+
|
|
22
|
+
const applyModificationsSkipped = (ctx) => {
|
|
23
|
+
// Always apply back unstaged modifications when skipping backup
|
|
24
|
+
if (!ctx.shouldBackup) return false
|
|
25
|
+
// Should be skipped in case of git errors
|
|
26
|
+
if (ctx.errors.has(GitError)) {
|
|
27
|
+
return GIT_ERROR
|
|
28
|
+
}
|
|
29
|
+
// Should be skipped when tasks fail
|
|
30
|
+
if (ctx.errors.has(TaskError)) {
|
|
31
|
+
return TASK_ERROR
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const restoreUnstagedChangesSkipped = (ctx) => {
|
|
36
|
+
// Should be skipped in case of git errors
|
|
37
|
+
if (ctx.errors.has(GitError)) {
|
|
38
|
+
return GIT_ERROR
|
|
39
|
+
}
|
|
40
|
+
// Should be skipped when tasks fail
|
|
41
|
+
if (ctx.errors.has(TaskError)) {
|
|
42
|
+
return TASK_ERROR
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const restoreOriginalStateEnabled = (ctx) =>
|
|
47
|
+
ctx.shouldBackup &&
|
|
48
|
+
(ctx.errors.has(TaskError) ||
|
|
49
|
+
ctx.errors.has(ApplyEmptyCommitError) ||
|
|
50
|
+
ctx.errors.has(RestoreUnstagedChangesError))
|
|
51
|
+
|
|
52
|
+
const restoreOriginalStateSkipped = (ctx) => {
|
|
53
|
+
// Should be skipped in case of unknown git errors
|
|
54
|
+
if (
|
|
55
|
+
ctx.errors.has(GitError) &&
|
|
56
|
+
!ctx.errors.has(ApplyEmptyCommitError) &&
|
|
57
|
+
!ctx.errors.has(RestoreUnstagedChangesError)
|
|
58
|
+
) {
|
|
59
|
+
return GIT_ERROR
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cleanupEnabled = (ctx) => ctx.shouldBackup
|
|
64
|
+
|
|
65
|
+
const cleanupSkipped = (ctx) => {
|
|
66
|
+
// Should be skipped in case of unknown git errors
|
|
67
|
+
if (
|
|
68
|
+
ctx.errors.has(GitError) &&
|
|
69
|
+
!ctx.errors.has(ApplyEmptyCommitError) &&
|
|
70
|
+
!ctx.errors.has(RestoreUnstagedChangesError)
|
|
71
|
+
) {
|
|
72
|
+
return GIT_ERROR
|
|
73
|
+
}
|
|
74
|
+
// Should be skipped when reverting to original state fails
|
|
75
|
+
if (ctx.errors.has(RestoreOriginalStateError)) {
|
|
76
|
+
return GIT_ERROR
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
getInitialState,
|
|
82
|
+
hasPartiallyStagedFiles,
|
|
83
|
+
applyModificationsSkipped,
|
|
84
|
+
restoreUnstagedChangesSkipped,
|
|
85
|
+
restoreOriginalStateEnabled,
|
|
86
|
+
restoreOriginalStateSkipped,
|
|
87
|
+
cleanupEnabled,
|
|
88
|
+
cleanupSkipped,
|
|
89
|
+
}
|
package/lib/symbols.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const ApplyEmptyCommitError = Symbol('ApplyEmptyCommitError')
|
|
4
|
+
const GetBackupStashError = Symbol('GetBackupStashError')
|
|
5
|
+
const GetStagedFilesError = Symbol('GetStagedFilesError')
|
|
6
|
+
const GitError = Symbol('GitError')
|
|
7
|
+
const GitRepoError = Symbol('GitRepoError')
|
|
8
|
+
const HideUnstagedChangesError = Symbol('HideUnstagedChangesError')
|
|
9
|
+
const RestoreMergeStatusError = Symbol('RestoreMergeStatusError')
|
|
10
|
+
const RestoreOriginalStateError = Symbol('RestoreOriginalStateError')
|
|
11
|
+
const RestoreUnstagedChangesError = Symbol('RestoreUnstagedChangesError')
|
|
12
|
+
const TaskError = Symbol('TaskError')
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
ApplyEmptyCommitError,
|
|
16
|
+
GetBackupStashError,
|
|
17
|
+
GetStagedFilesError,
|
|
18
|
+
GitError,
|
|
19
|
+
GitRepoError,
|
|
20
|
+
HideUnstagedChangesError,
|
|
21
|
+
RestoreMergeStatusError,
|
|
22
|
+
RestoreOriginalStateError,
|
|
23
|
+
RestoreUnstagedChangesError,
|
|
24
|
+
TaskError,
|
|
25
|
+
}
|
package/lib/validateConfig.js
CHANGED
|
@@ -15,7 +15,7 @@ const TEST_DEPRECATED_KEYS = new Map([
|
|
|
15
15
|
['ignore', (key) => Array.isArray(key)],
|
|
16
16
|
['subTaskConcurrency', (key) => typeof key === 'number'],
|
|
17
17
|
['renderer', (key) => typeof key === 'string'],
|
|
18
|
-
['relative', (key) => typeof key === 'boolean']
|
|
18
|
+
['relative', (key) => typeof key === 'boolean'],
|
|
19
19
|
])
|
|
20
20
|
|
|
21
21
|
const formatError = (helpMsg) => `● Validation Error:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lint-staged",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.2",
|
|
4
4
|
"description": "Lint files staged by git",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/okonet/lint-staged",
|
|
@@ -21,9 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"cz": "git-cz",
|
|
24
|
-
"lint
|
|
25
|
-
"lint": "npm run lint:base -- .",
|
|
26
|
-
"lint:fix": "npm run lint -- --fix",
|
|
24
|
+
"lint": "eslint .",
|
|
27
25
|
"pretest": "npm run lint",
|
|
28
26
|
"test": "jest --coverage",
|
|
29
27
|
"test:watch": "jest --watch"
|
|
@@ -40,7 +38,7 @@
|
|
|
40
38
|
"debug": "^4.1.1",
|
|
41
39
|
"dedent": "^0.7.0",
|
|
42
40
|
"execa": "^4.0.0",
|
|
43
|
-
"
|
|
41
|
+
"listr2": "1.3.8",
|
|
44
42
|
"log-symbols": "^3.0.0",
|
|
45
43
|
"micromatch": "^4.0.2",
|
|
46
44
|
"normalize-path": "^3.0.0",
|
|
@@ -52,18 +50,14 @@
|
|
|
52
50
|
"@babel/core": "^7.9.0",
|
|
53
51
|
"@babel/plugin-proposal-object-rest-spread": "^7.9.5",
|
|
54
52
|
"@babel/preset-env": "^7.9.5",
|
|
55
|
-
"babel-eslint": "
|
|
53
|
+
"babel-eslint": "10.1.0",
|
|
56
54
|
"babel-jest": "^25.3.0",
|
|
57
55
|
"consolemock": "^1.1.0",
|
|
58
56
|
"eslint": "^6.8.0",
|
|
59
|
-
"eslint-config-okonet": "^7.0.2",
|
|
60
57
|
"eslint-config-prettier": "^6.10.1",
|
|
61
|
-
"eslint-plugin-flowtype": "^4.7.0",
|
|
62
58
|
"eslint-plugin-import": "^2.20.2",
|
|
63
|
-
"eslint-plugin-jsx-a11y": "^6.2.3",
|
|
64
59
|
"eslint-plugin-node": "^11.1.0",
|
|
65
60
|
"eslint-plugin-prettier": "^3.1.3",
|
|
66
|
-
"eslint-plugin-react": "^7.19.0",
|
|
67
61
|
"fs-extra": "^9.0.0",
|
|
68
62
|
"husky": "^4.2.5",
|
|
69
63
|
"jest": "^25.3.0",
|
package/lib/printErrors.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// istanbul ignore next
|
|
4
|
-
// Work-around for duplicated error logs, see #142
|
|
5
|
-
const errMsg = (err) => (err.privateMsg != null ? err.privateMsg : err.message)
|
|
6
|
-
|
|
7
|
-
module.exports = function printErrors(errorInstance, logger) {
|
|
8
|
-
if (Array.isArray(errorInstance.errors)) {
|
|
9
|
-
errorInstance.errors.forEach((lintError) => {
|
|
10
|
-
logger.error(errMsg(lintError))
|
|
11
|
-
})
|
|
12
|
-
} else {
|
|
13
|
-
logger.error(errMsg(errorInstance))
|
|
14
|
-
}
|
|
15
|
-
}
|