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 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
- -h, --help output usage information
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 => (filenames.length > 10 ? 'eslint .' : `eslint ${filenames.join(' ')}`)
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
- debug: false
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
- config: {
439
- '*.js': 'eslint --fix'
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
- quiet: false,
445
- debug: false
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
  ```
@@ -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 [files]
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(files, chunkCount)
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
@@ -59,5 +59,5 @@ const writeFile = async (filename, buffer) => {
59
59
  module.exports = {
60
60
  readFile,
61
61
  unlink,
62
- writeFile
62
+ writeFile,
63
63
  }
@@ -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
@@ -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.gitError = true
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, matchedFiles, maxArgLength }) {
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.matchedFiles = matchedFiles
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.gitGetBackupStashError = true
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
- throw new Error('Merge state could not be restored due to an error!')
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, shouldBackup) {
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 `--keep-index` flag resurrects deleted files
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
- // the `git stash` clears metadata about a possible git merge
195
- // Manually check and backup if necessary
196
- await this.backupMergeStatus()
197
-
198
- // Save stash of original state
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.gitHideUnstagedChangesError = true
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.gitApplyEmptyCommitError = true
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.gitRestoreOriginalStateError = true
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
- const printErrors = require('./printErrors')
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
- { allowEmpty, concurrent, config, debug, maxArgLength, stash, quiet, relative, shell },
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('tasks were executed successfully!')
123
+ debugLog('Tasks were executed successfully!')
124
+ printTaskOutput(ctx, logger)
106
125
  return true
107
126
  } catch (runAllError) {
108
- printErrors(runAllError, logger)
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) {
@@ -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
  }
@@ -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
@@ -1,57 +1,66 @@
1
1
  'use strict'
2
2
 
3
- const chalk = require('chalk')
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 successMsg = (linter) => `${symbols.success} ${linter} passed!`
12
+ const getTag = ({ code, killed, signal }) => signal || (killed && 'KILLED') || code || 'FAILED'
12
13
 
13
14
  /**
14
- * Create and returns an error instance with a given message.
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} message
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
- function throwError(message) {
23
- const err = new Error()
24
- err.privateMsg = `\n\n\n${message}`
25
- return err
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 failure message dependding on process result.
47
+ * Create a error output dependding on process result.
30
48
  *
31
- * @param {string} linter
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} context (see https://github.com/SamVerschueren/listr#context)
56
+ * @param {Object} ctx
39
57
  * @returns {Error}
40
58
  */
41
- function makeErr(linter, result, context = {}) {
42
- context.taskError = true
43
- const { stdout, stderr, killed, signal } = result
44
- if (killed || (signal && signal !== '')) {
45
- return throwError(
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({ command, files, gitDir, isFn, relative, shell = false }) {
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 promise = shell
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(cmd, result, ctx)
108
+ throw makeErr(command, result, ctx)
92
109
  }
93
110
 
94
- return successMsg(cmd)
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 chalk = require('chalk')
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
- const debugLog = require('debug')('lint-staged:run')
18
-
19
- const getRenderer = ({ debug, quiet }) => {
20
- if (quiet) return 'silent'
21
- // Better support for dumb terminals: https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals
22
- const isDumbTerminal = process.env.TERM === 'dumb'
23
- if (debug || isDumbTerminal) return 'verbose'
24
- return 'update'
25
- }
26
-
27
- const MESSAGES = {
28
- TASK_ERROR: 'Skipped because of errors from tasks.',
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) throw new Error('Current directory is not a git directory!')
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
- const shouldBackup = hasInitialCommit && stash
106
- if (!shouldBackup) {
107
- const reason = hasInitialCommit ? '`--no-stash` was used' : 'there’s no initial commit yet'
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) throw new Error('Unable to get staged 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
- return logger.log(`${symbols.info} No staged files found.`)
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
- dateFormat: false,
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: (ctx = {}) => {
175
+ skip: () => {
184
176
  // Skip if the first step (backup) failed
185
- if (ctx.gitError) return MESSAGES.GIT_ERROR
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(`${symbols.warning} ${chalk.yellow(
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
- logger.log('No staged files match any of provided globs.')
204
- return 'No tasks to run.'
192
+ if (!quiet) ctx.output.push(NO_TASKS)
193
+ return ctx
205
194
  }
206
195
 
207
- const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFiles, maxArgLength })
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, shouldBackup)
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: (ctx) => ctx.hasPartiallyStagedFiles
216
+ enabled: hasPartiallyStagedFiles,
219
217
  },
220
218
  ...listrTasks,
221
219
  {
222
220
  title: 'Applying modifications...',
223
221
  task: (ctx) => git.applyModifications(ctx),
224
- // Always apply back unstaged modifications when skipping backup
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: (ctx) => ctx.hasPartiallyStagedFiles,
231
- skip: shouldSkipApplyModifications
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: (ctx) =>
237
- shouldBackup &&
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: () => shouldBackup,
245
- skip: shouldSkipCleanup
246
- }
239
+ enabled: cleanupEnabled,
240
+ skip: cleanupSkipped,
241
+ },
247
242
  ],
248
243
  listrOptions
249
244
  )
250
245
 
251
- try {
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
- throw error
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
+ }
@@ -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.1.6",
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:base": "eslint --rule \"prettier/prettier: 2\"",
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
- "listr": "^0.14.3",
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": "^10.1.0",
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",
@@ -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
- }