lint-staged 16.2.0 → 16.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
@@ -127,9 +127,9 @@ Options:
127
127
 
128
128
  Any lost modifications can be restored from a git stash:
129
129
 
130
- > git stash list
131
- stash@{0}: automatic lint-staged backup
132
- > git stash apply --index stash@{0}
130
+ > git stash list --format="%h %s"
131
+ h0a0s0h0 On main: lint-staged automatic backup
132
+ > git apply --index h0a0s0h0
133
133
  ```
134
134
 
135
135
  #### `--allow-empty`
@@ -6,7 +6,7 @@ import { Option, program } from 'commander'
6
6
 
7
7
  import { createDebug, enableDebug } from '../lib/debug.js'
8
8
  import lintStaged from '../lib/index.js'
9
- import { CONFIG_STDIN_ERROR, RESTORE_STASH_EXAMPLE } from '../lib/messages.js'
9
+ import { CONFIG_STDIN_ERROR, restoreStashExample } from '../lib/messages.js'
10
10
  import { readStdin } from '../lib/readStdin.js'
11
11
  import { getVersion } from '../lib/version.js'
12
12
 
@@ -108,7 +108,7 @@ program
108
108
  .addOption(
109
109
  new Option('--hide-unstaged', 'hide all unstaged changes, instead of just partially staged')
110
110
  .default(false)
111
- .implies({ hidePartiallyStaged: true })
111
+ .implies({ hidePartiallyStaged: false })
112
112
  )
113
113
 
114
114
  .addOption(new Option('-q, --quiet', 'disable lint-staged’s own console output').default(false))
@@ -120,7 +120,7 @@ program
120
120
  ).default(false)
121
121
  )
122
122
 
123
- .addHelpText('afterAll', '\n' + RESTORE_STASH_EXAMPLE)
123
+ .addHelpText('afterAll', '\n' + restoreStashExample())
124
124
 
125
125
  const cliOptions = program.parse(process.argv).opts()
126
126
 
@@ -6,9 +6,10 @@ import { createDebug } from './debug.js'
6
6
  import { execGit } from './execGit.js'
7
7
  import { readFile, unlink, writeFile } from './file.js'
8
8
  import { getDiffCommand } from './getDiffCommand.js'
9
+ import { parseGitZOutput } from './parseGitZOutput.js'
9
10
  import {
10
11
  ApplyEmptyCommitError,
11
- ExitCodeError,
12
+ FailOnChangesError,
12
13
  GetBackupStashError,
13
14
  GitError,
14
15
  HideUnstagedChangesError,
@@ -69,6 +70,12 @@ const handleError = (error, ctx, symbol) => {
69
70
 
70
71
  const calculateSha256 = (input) => crypto.createHash('sha256').update(input, 'utf-8').digest('hex')
71
72
 
73
+ /**
74
+ * The lines are wrapped in double quotes
75
+ * @returns {string[]}
76
+ */
77
+ const cleanGitStashOutput = (lines) => lines.map((line) => line.replace(/^"(.*)"$/, '$1'))
78
+
72
79
  export class GitWorkflow {
73
80
  /**
74
81
  * @param {Object} opts
@@ -115,11 +122,12 @@ export class GitWorkflow {
115
122
  * Get name of backup stash
116
123
  */
117
124
  async getBackupStash(ctx) {
118
- const stashes = await this.execGit(['stash', 'list'])
125
+ /** Print stash list with short hash and subject */
126
+ const stashes = await this.execGit(['stash', 'list', '--format="%h %s"', '-z'])
127
+ .then(parseGitZOutput)
128
+ .then(cleanGitStashOutput)
119
129
 
120
- const index = stashes
121
- .split('\n')
122
- .findIndex((line) => line.includes(STASH) && line.includes(ctx.backupHash))
130
+ const index = stashes.findIndex((line) => line.startsWith(ctx.backupHash))
123
131
 
124
132
  if (index === -1) {
125
133
  ctx.errors.add(GetBackupStashError)
@@ -231,20 +239,40 @@ export class GitWorkflow {
231
239
  // - in git versions =< 2.13.0 the `git stash --keep-index` option resurrects deleted files
232
240
  // - git stash can't infer RD or MD states correctly, and will lose the deletion
233
241
  this.deletedFiles = await this.getDeletedFiles()
242
+ }
234
243
 
235
- // Save stash of all staged files.
236
- // The `stash create` command creates a dangling commit without removing any files,
237
- // and `stash store` saves it as an actual stash.
238
- const stashHash = await this.execGit(['stash', 'create'])
239
- ctx.backupHash = await this.execGit(['rev-parse', '--short', stashHash])
240
- await this.execGit([
241
- 'stash',
242
- 'store',
243
- '--quiet',
244
- '--message',
245
- `${STASH} (${ctx.backupHash})`,
246
- ctx.backupHash,
247
- ])
244
+ if (ctx.shouldHideUnstaged) {
245
+ this.unstagedFiles = await this.getUnstagedFiles({ onlyPartial: false })
246
+ ctx.hasFilesToHide = !!this.unstagedFiles
247
+ } else if (ctx.shouldHidePartiallyStaged) {
248
+ this.unstagedFiles = await this.getUnstagedFiles({ onlyPartial: true })
249
+ ctx.hasFilesToHide = !!this.unstagedFiles
250
+ }
251
+
252
+ if (this.unstagedFiles) {
253
+ const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
254
+ ctx.unstagedPatch = unstagedPatch
255
+ const files = processRenames(this.unstagedFiles)
256
+ await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
257
+ }
258
+
259
+ if (ctx.shouldBackup) {
260
+ if (ctx.shouldHideUnstaged) {
261
+ /** Save stash of all changes, clearing the working tree but keeping staged files as-is */
262
+ await this.execGit(['stash', 'push', '--keep-index', '--message', STASH])
263
+ /** Print stash list with short hash and subject */
264
+ const stashes = await this.execGit(['stash', 'list', '--format="%h %s"', '-z'])
265
+ .then(parseGitZOutput)
266
+ .then(cleanGitStashOutput)
267
+
268
+ /** The stash line starts with the short hash, so we split from space and choose the first part */
269
+ ctx.backupHash = stashes.find((line) => line.includes(STASH))?.split(' ')[0]
270
+ } else {
271
+ /** Save stash of all changes, keeping all files as-is */
272
+ const stashHash = await this.execGit(['stash', 'create'])
273
+ ctx.backupHash = await this.execGit(['rev-parse', '--short', stashHash])
274
+ await this.execGit(['stash', 'store', '--quiet', '--message', STASH, ctx.backupHash])
275
+ }
248
276
 
249
277
  task.title = `Backed up original state in git stash (${ctx.backupHash})`
250
278
  debugLog(task.title)
@@ -258,32 +286,12 @@ export class GitWorkflow {
258
286
  this.unstagedDiffSha256 = calculateSha256(diff)
259
287
  debugLog('SHA-256 hash of unstaged changes is %S', this.unstagedDiffSha256)
260
288
  }
261
-
262
- if (ctx.shouldHideUnstaged || ctx.shouldHidePartiallyStaged) {
263
- // Unstaged changes to these files should be hidden before the tasks run.
264
- this.unstagedFiles = await this.getUnstagedFiles({
265
- onlyPartial: ctx.shouldHideUnstaged ? false : ctx.shouldHidePartiallyStaged,
266
- })
267
-
268
- if (this.unstagedFiles) {
269
- ctx.hasFilesToHide = true
270
- const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
271
- ctx.unstagedPatch = unstagedPatch
272
- const files = processRenames(this.unstagedFiles)
273
- await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
274
- } else {
275
- ctx.hasFilesToHide = false
276
- }
277
- }
278
289
  } catch (error) {
279
290
  handleError(error, ctx)
280
291
  }
281
292
  }
282
293
 
283
- /**
284
- * Remove unstaged changes to all partially staged files, to avoid tasks from seeing them
285
- */
286
- async hideUnstagedChanges(ctx) {
294
+ async hidePartiallyStagedChanges(ctx) {
287
295
  try {
288
296
  const files = processRenames(this.unstagedFiles, false)
289
297
  await this.execGit(['checkout', '--force', '--', ...files])
@@ -309,7 +317,7 @@ export class GitWorkflow {
309
317
  const diffSha256 = calculateSha256(diff)
310
318
  debugLog('SHA-256 hash of changes after tasks is %S', this.diffSha256)
311
319
  if (this.unstagedDiffSha256 !== diffSha256) {
312
- ctx.errors.add(ExitCodeError)
320
+ ctx.errors.add(FailOnChangesError)
313
321
  throw new Error('Tasks modified files and --fail-on-changes was used!')
314
322
  }
315
323
  }
@@ -368,8 +376,8 @@ export class GitWorkflow {
368
376
  debugLog('Error while restoring changes:')
369
377
  debugLog(applyError)
370
378
  debugLog('Retrying with 3-way merge')
379
+ // Retry with a 3-way merge if normal apply fails
371
380
  try {
372
- // Retry with a 3-way merge if normal apply fails
373
381
  await this.execGit(['apply', ...GIT_APPLY_ARGS, '--3way', unstagedPatch])
374
382
  } catch (threeWayApplyError) {
375
383
  debugLog('Error while restoring unstaged changes using 3-way merge:')
package/lib/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- type SyncGenerateTask = (stagedFileNames: string[]) => string | string[]
1
+ type SyncGenerateTask = (stagedFileNames: readonly string[]) => string | string[]
2
2
 
3
- type AsyncGenerateTask = (stagedFileNames: string[]) => Promise<string | string[]>
3
+ type AsyncGenerateTask = (stagedFileNames: readonly string[]) => Promise<string | string[]>
4
4
 
5
5
  type GenerateTask = SyncGenerateTask | AsyncGenerateTask
6
6
 
7
7
  type TaskFunction = {
8
8
  title: string
9
- task: (stagedFileNames: string[]) => void | Promise<void>
9
+ task: (stagedFileNames: readonly string[]) => void | Promise<void>
10
10
  }
11
11
 
12
12
  export type Configuration =
@@ -19,6 +19,11 @@ export type Options = {
19
19
  * @default false
20
20
  */
21
21
  allowEmpty?: boolean
22
+ /**
23
+ * Enable or disable ANSI color codes in output. By default value is auto-detected
24
+ * and controlled by `FORCE_COLOR` or `NO_COLOR` env variables.
25
+ */
26
+ color?: boolean
22
27
  /**
23
28
  * The number of tasks to run concurrently, or `false` to run tasks serially
24
29
  * @default true
@@ -61,7 +66,7 @@ export type Options = {
61
66
  * Fail with exit code 1 when tasks modify tracked files
62
67
  * @default false
63
68
  */
64
- failOnChanges?: true
69
+ failOnChanges?: boolean
65
70
  /**
66
71
  * Maximum argument string length, by default automatically detected
67
72
  */
@@ -104,12 +109,13 @@ export type Options = {
104
109
  verbose?: boolean
105
110
  }
106
111
 
107
- type LogFunction = (...params: any) => void
112
+ type LogFunction = typeof console.log
108
113
 
109
114
  type Logger = {
110
115
  log: LogFunction
111
116
  warn: LogFunction
112
117
  error: LogFunction
118
+ debug: LogFunction
113
119
  }
114
120
 
115
121
  /**
package/lib/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  NO_CONFIGURATION,
7
7
  PREVENTED_EMPTY_COMMIT,
8
8
  PREVENTED_TASK_MODIFICATIONS,
9
- RESTORE_STASH_EXAMPLE,
9
+ restoreStashExample,
10
10
  UNSTAGED_CHANGES_BACKUP_STASH_LOCATION,
11
11
  } from './messages.js'
12
12
  import { printTaskOutput } from './printTaskOutput.js'
@@ -15,7 +15,7 @@ import { cleanupSkipped } from './state.js'
15
15
  import {
16
16
  ApplyEmptyCommitError,
17
17
  ConfigNotFoundError,
18
- ExitCodeError,
18
+ FailOnChangesError,
19
19
  GetBackupStashError,
20
20
  GitError,
21
21
  RestoreUnstagedChangesError,
@@ -51,7 +51,7 @@ const getMaxArgLength = () => {
51
51
  *
52
52
  * @param {object} options
53
53
  * @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
54
- * @param {Object} [options.color] - Enable ANSI colors in output
54
+ * @param {boolean} [options.color] - Enable or disable ANSI color codes in output.
55
55
  * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
56
56
  * @param {object} [options.config] - Object with configuration for programmatic API
57
57
  * @param {string} [options.configPath] - Path to configuration file
@@ -86,8 +86,8 @@ const lintStaged = async (
86
86
  diff,
87
87
  diffFilter,
88
88
  failOnChanges = false,
89
- hidePartiallyStaged = true,
90
89
  hideUnstaged = false,
90
+ hidePartiallyStaged = !hideUnstaged,
91
91
  maxArgLength = getMaxArgLength() / 2,
92
92
  quiet = false,
93
93
  relative = false,
@@ -155,8 +155,9 @@ const lintStaged = async (
155
155
  logger.error(NO_CONFIGURATION)
156
156
  } else if (ctx.errors.has(ApplyEmptyCommitError)) {
157
157
  logger.warn(PREVENTED_EMPTY_COMMIT)
158
- } else if (ctx.errors.has(ExitCodeError)) {
159
- logger.warn(PREVENTED_TASK_MODIFICATIONS)
158
+ } else if (ctx.errors.has(FailOnChangesError)) {
159
+ logger.warn(PREVENTED_TASK_MODIFICATIONS + '\n')
160
+ logger.warn(restoreStashExample(ctx.backupHash))
160
161
  } else if (ctx.errors.has(RestoreUnstagedChangesError)) {
161
162
  logger.warn(UNSTAGED_CHANGES_BACKUP_STASH_LOCATION)
162
163
  logger.warn(ctx.unstagedPatch)
@@ -167,7 +168,7 @@ const lintStaged = async (
167
168
  logger.error(GIT_ERROR)
168
169
  if (ctx.shouldBackup) {
169
170
  // No sense to show this if the backup stash itself is missing.
170
- logger.error(RESTORE_STASH_EXAMPLE + '\n')
171
+ logger.error(restoreStashExample(ctx.backupHash) + '\n')
171
172
  }
172
173
  }
173
174
 
package/lib/loadConfig.js CHANGED
@@ -2,25 +2,28 @@
2
2
 
3
3
  import fs from 'node:fs/promises'
4
4
  import path from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
5
6
 
6
7
  import YAML from 'yaml'
7
8
 
8
9
  import { CONFIG_NAME, PACKAGE_JSON_FILE, PACKAGE_YAML_FILES } from './configFiles.js'
9
10
  import { createDebug } from './debug.js'
10
- import { dynamicImport } from './dynamicImport.js'
11
11
  import { failedToLoadConfig } from './messages.js'
12
12
  import { resolveConfig } from './resolveConfig.js'
13
13
 
14
14
  const debugLog = createDebug('lint-staged:loadConfig')
15
15
 
16
- const jsonParse = (filePath, content) => {
17
- const isPackageFile = PACKAGE_JSON_FILE.includes(path.basename(filePath))
16
+ const readFile = async (filename) => fs.readFile(path.resolve(filename), 'utf-8')
17
+
18
+ const jsonParse = async (filename) => {
19
+ const isPackageFile = PACKAGE_JSON_FILE.includes(path.basename(filename))
18
20
  try {
21
+ const content = await readFile(filename)
19
22
  const json = JSON.parse(content)
20
23
  return isPackageFile ? json[CONFIG_NAME] : json
21
24
  } catch (error) {
22
- if (path.basename(filePath) === PACKAGE_JSON_FILE) {
23
- debugLog('Ignoring invalid package file `%s` with content:\n%s', filePath, content)
25
+ if (path.basename(filename) === PACKAGE_JSON_FILE) {
26
+ debugLog('Ignoring invalid JSON file %s', filename)
24
27
  return undefined
25
28
  }
26
29
 
@@ -28,14 +31,15 @@ const jsonParse = (filePath, content) => {
28
31
  }
29
32
  }
30
33
 
31
- const yamlParse = (filePath, content) => {
32
- const isPackageFile = PACKAGE_YAML_FILES.includes(path.basename(filePath))
34
+ const yamlParse = async (filename) => {
35
+ const isPackageFile = PACKAGE_YAML_FILES.includes(path.basename(filename))
33
36
  try {
37
+ const content = await readFile(filename)
34
38
  const yaml = YAML.parse(content)
35
39
  return isPackageFile ? yaml[CONFIG_NAME] : yaml
36
40
  } catch (error) {
37
41
  if (isPackageFile) {
38
- debugLog('Ignoring invalid package file `%s` with content:\n%s', filePath, content)
42
+ debugLog('Ignoring invalid YAML file %s', filename)
39
43
  return undefined
40
44
  }
41
45
 
@@ -43,6 +47,8 @@ const yamlParse = (filePath, content) => {
43
47
  }
44
48
  }
45
49
 
50
+ export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)
51
+
46
52
  const NO_EXT = 'noExt'
47
53
 
48
54
  /**
@@ -64,43 +70,30 @@ const loaders = {
64
70
  '.yml': yamlParse,
65
71
  }
66
72
 
67
- const readFile = async (filepath) => {
68
- const absolutePath = path.resolve(filepath)
69
- return fs.readFile(absolutePath, 'utf-8')
70
- }
71
-
72
- const loadConfigByExt = async (filepath) => {
73
- filepath = path.resolve(filepath)
73
+ const loadConfigByExt = async (filename) => {
74
+ const filepath = path.resolve(filename)
74
75
  const ext = path.extname(filepath) || NO_EXT
75
76
  const loader = loaders[ext]
77
+ const config = await loader(filepath)
76
78
 
77
- /**
78
- * No need to read file contents when loader only takes in the filepath argument
79
- * and reads itself; this is for `lilconfig` compatibility
80
- */
81
- const content = loader.length > 1 ? await readFile(filepath) : undefined
82
-
83
- return {
84
- config: await loader(filepath, content),
85
- filepath,
86
- }
79
+ return { config, filepath }
87
80
  }
88
81
 
89
- /**
90
- * @param {object} options
91
- * @param {string} [options.configPath] - Explicit path to a config file
92
- */
93
- export const loadConfig = async ({ configPath }, logger) => {
82
+ /** @param {string} configPath */
83
+ export const loadConfig = async (configPath, logger) => {
94
84
  try {
95
85
  debugLog('Loading configuration from `%s`...', configPath)
96
- const result = configPath ? await loadConfigByExt(resolveConfig(configPath)) : undefined
97
- if (!result) return {}
86
+ const result = await loadConfigByExt(resolveConfig(configPath))
98
87
 
99
88
  // config is a promise when using the `dynamicImport` loader
100
89
  const config = (await result.config) ?? null
101
90
  const filepath = result.filepath
102
91
 
103
- debugLog('Successfully loaded config from `%s`:\n%O', filepath, config)
92
+ if (config) {
93
+ debugLog('Successfully loaded config from `%s`:\n%O', filepath, config)
94
+ } else {
95
+ debugLog('Found no config in %s', filepath)
96
+ }
104
97
 
105
98
  return { config, filepath }
106
99
  } catch (error) {
package/lib/messages.js CHANGED
@@ -66,11 +66,13 @@ export const PREVENTED_EMPTY_COMMIT = `
66
66
  Use the --allow-empty option to continue, or check your task configuration`)}
67
67
  `
68
68
 
69
- export const RESTORE_STASH_EXAMPLE = `Any lost modifications can be restored from a git stash:
69
+ export const restoreStashExample = (
70
+ hash = 'h0a0s0h0'
71
+ ) => `Any lost modifications can be restored from a git stash:
70
72
 
71
- > git stash list
72
- stash@{0}: automatic lint-staged backup
73
- > git stash apply --index stash@{0}`
73
+ > git stash list --format="%h %s"
74
+ ${hash} On main: lint-staged automatic backup
75
+ > git apply --index ${hash}`
74
76
 
75
77
  export const CONFIG_STDIN_ERROR = red(`${error} Failed to read config from stdin.`)
76
78
 
package/lib/runAll.js CHANGED
@@ -36,7 +36,8 @@ import {
36
36
  restoreOriginalStateEnabled,
37
37
  restoreOriginalStateSkipped,
38
38
  restoreUnstagedChangesSkipped,
39
- shouldHideUnstagedFiles,
39
+ shouldHidePartiallyStagedFiles,
40
+ shouldRestoreUnstagedChanges,
40
41
  } from './state.js'
41
42
  import { ConfigNotFoundError, GetStagedFilesError, GitError, GitRepoError } from './symbols.js'
42
43
 
@@ -54,7 +55,7 @@ const createError = (ctx, cause) =>
54
55
  *
55
56
  * @param {object} options
56
57
  * @param {boolean} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
57
- * @param {Object} [options.color] - Enable ANSI colors in output
58
+ * @param {boolean} [options.color] - Enable or disable ANSI color codes in output.
58
59
  * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
59
60
  * @param {Object} [options.configObject] - Explicit config object from the js API
60
61
  * @param {string} [options.configPath] - Explicit path to a config file
@@ -88,8 +89,8 @@ export const runAll = async (
88
89
  diff,
89
90
  diffFilter,
90
91
  failOnChanges = false,
91
- hidePartiallyStaged = true,
92
92
  hideUnstaged = false,
93
+ hidePartiallyStaged = !hideUnstaged,
93
94
  maxArgLength,
94
95
  quiet = false,
95
96
  relative = false,
@@ -135,7 +136,7 @@ export const runAll = async (
135
136
  logger.warn(skippingBackup(hasInitialCommit, diff))
136
137
  }
137
138
 
138
- if (!ctx.shouldHidePartiallyStaged && !quiet) {
139
+ if (!ctx.shouldHidePartiallyStaged && !ctx.shouldHideUnstaged && !quiet) {
139
140
  logger.warn(SKIPPING_HIDE_PARTIALLY_CHANGED)
140
141
  }
141
142
 
@@ -330,8 +331,8 @@ export const runAll = async (
330
331
  },
331
332
  {
332
333
  title: 'Hiding unstaged changes to partially staged files...',
333
- task: (ctx) => git.hideUnstagedChanges(ctx),
334
- enabled: shouldHideUnstagedFiles,
334
+ task: (ctx) => git.hidePartiallyStagedChanges(ctx),
335
+ enabled: shouldHidePartiallyStagedFiles,
335
336
  },
336
337
  {
337
338
  title: `Running tasks for ${diff ? 'changed' : 'staged'} files...`,
@@ -344,9 +345,9 @@ export const runAll = async (
344
345
  skip: applyModificationsSkipped,
345
346
  },
346
347
  {
347
- title: 'Restoring unstaged changes to partially staged files...',
348
+ title: 'Restoring unstaged changes...',
348
349
  task: (ctx) => git.restoreUnstagedChanges(ctx),
349
- enabled: shouldHideUnstagedFiles,
350
+ enabled: shouldRestoreUnstagedChanges,
350
351
  skip: restoreUnstagedChangesSkipped,
351
352
  },
352
353
  {
@@ -19,6 +19,8 @@ const CONFIG_PATHSPEC = CONFIG_FILE_NAMES.map((f) => `:(glob)**/${f}`)
19
19
 
20
20
  const numberOfLevels = (file) => file.split('/').length
21
21
 
22
+ const sortAlphabetically = (a, b) => a.localeCompare(b)
23
+
22
24
  const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
23
25
 
24
26
  /**
@@ -43,22 +45,20 @@ const listConfigFilesFromGit = async ({ cwd, topLevelDir }) =>
43
45
  )
44
46
  .then(parseGitZOutput)
45
47
  .then((lines) => {
46
- const possibleConfigFiles = lines
47
- .flatMap((line) => {
48
- /**
49
- * Leave out lines starting with "S " to ignore not-checked-out files in a sparse repo.
50
- * The "S" status means a tracked file that is "skip-worktree"
51
- * @see https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--t
52
- */
53
- if (line.startsWith('S ')) {
54
- return []
55
- }
48
+ const possibleConfigFiles = lines.flatMap((line) => {
49
+ /**
50
+ * Leave out lines starting with "S " to ignore not-checked-out files in a sparse repo.
51
+ * The "S" status means a tracked file that is "skip-worktree"
52
+ * @see https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--t
53
+ */
54
+ if (line.startsWith('S ')) {
55
+ return []
56
+ }
56
57
 
57
- const relativePath = line.replace(/^[HSMRCK?U] /, '')
58
- const absolutePath = normalizePath(path.join(topLevelDir, relativePath))
59
- return [absolutePath]
60
- })
61
- .sort(sortDeepestParth)
58
+ const relativePath = line.replace(/^[HSMRCK?U] /, '')
59
+ const absolutePath = normalizePath(path.join(topLevelDir, relativePath))
60
+ return [absolutePath]
61
+ })
62
62
 
63
63
  debugLog('Found possible config files from git:', possibleConfigFiles)
64
64
 
@@ -91,7 +91,7 @@ export const listConfigFilesFromFs = async ({ cwd }) => {
91
91
 
92
92
  if (possibleConfigFiles.length > 0) {
93
93
  debugLog('Found possible config files from filesystem:', possibleConfigFiles)
94
- return possibleConfigFiles.sort((a, b) => a.localeCompare(b))
94
+ return possibleConfigFiles
95
95
  }
96
96
 
97
97
  const parentDir = path.dirname(cwd)
@@ -131,27 +131,35 @@ export const searchConfigs = async (
131
131
  if (configPath) {
132
132
  debugLog('Using single configuration path...')
133
133
 
134
- const { config, filepath } = await loadConfig({ configPath }, logger)
134
+ const { config, filepath } = await loadConfig(configPath, logger)
135
135
 
136
136
  if (!config) return {}
137
137
  return { [configPath]: validateConfig(config, filepath, logger) }
138
138
  }
139
139
 
140
- let possibleConfigFiles = await listConfigFilesFromGit({ cwd, topLevelDir })
141
- if (possibleConfigFiles.length === 0) {
142
- possibleConfigFiles = await listConfigFilesFromFs({ cwd })
140
+ const possibleConfigFiles = new Set()
141
+
142
+ const addToSet = (files) => {
143
+ files.forEach((f) => {
144
+ possibleConfigFiles.add(f)
145
+ })
143
146
  }
144
147
 
148
+ await Promise.all([
149
+ listConfigFilesFromGit({ cwd, topLevelDir }).then(addToSet),
150
+ listConfigFilesFromFs({ cwd }).then(addToSet),
151
+ ])
152
+
145
153
  /** Create object with key as config file, and value as null */
146
- const configs = possibleConfigFiles.reduce(
147
- (acc, configPath) => Object.assign(acc, { [configPath]: null }),
148
- {}
149
- )
154
+ const configs = Array.from(possibleConfigFiles)
155
+ .sort(sortAlphabetically)
156
+ .sort(sortDeepestParth)
157
+ .reduce((acc, configPath) => Object.assign(acc, { [configPath]: null }), {})
150
158
 
151
159
  /** Load and validate all configs to the above object */
152
160
  await Promise.all(
153
161
  Object.keys(configs).map((configPath) =>
154
- loadConfig({ configPath }, logger).then(({ config, filepath }) => {
162
+ loadConfig(configPath, logger).then(({ config, filepath }) => {
155
163
  if (config) {
156
164
  if (configPath !== filepath) {
157
165
  debugLog('Config file "%s" resolved to "%s"', configPath, filepath)
package/lib/state.js CHANGED
@@ -2,7 +2,7 @@ import EventEmitter from 'events'
2
2
 
3
3
  import { GIT_ERROR, TASK_ERROR } from './messages.js'
4
4
  import {
5
- ExitCodeError,
5
+ FailOnChangesError,
6
6
  GitError,
7
7
  RestoreOriginalStateError,
8
8
  RestoreUnstagedChangesError,
@@ -10,14 +10,15 @@ import {
10
10
  } from './symbols.js'
11
11
 
12
12
  export const getInitialState = ({
13
- hidePartiallyStaged = true,
14
13
  hideUnstaged = false,
14
+ hidePartiallyStaged = !hideUnstaged,
15
15
  quiet = false,
16
16
  revert = true,
17
17
  } = {}) => ({
18
18
  backupHash: null,
19
19
  errors: new Set([]),
20
20
  events: new EventEmitter(),
21
+ shouldFailOnChanges: null,
21
22
  hasFilesToHide: null,
22
23
  output: [],
23
24
  quiet,
@@ -28,16 +29,21 @@ export const getInitialState = ({
28
29
  unstagedPatch: null,
29
30
  })
30
31
 
31
- export const shouldHideUnstagedFiles = (ctx) =>
32
+ export const shouldHidePartiallyStagedFiles = (ctx) =>
33
+ ctx.shouldHidePartiallyStaged && ctx.hasFilesToHide
34
+
35
+ export const shouldRestoreUnstagedChanges = (ctx) =>
32
36
  (ctx.shouldHideUnstaged || ctx.shouldHidePartiallyStaged) && ctx.hasFilesToHide
33
37
 
34
38
  export const applyModificationsSkipped = (ctx) => {
35
39
  // Always apply back unstaged modifications when skipping revert or backup
36
40
  if (!ctx.shouldRevert || !ctx.shouldBackup) return false
41
+
37
42
  // Should be skipped in case of git errors
38
43
  if (ctx.errors.has(GitError)) {
39
44
  return GIT_ERROR
40
45
  }
46
+
41
47
  // Should be skipped when tasks fail
42
48
  if (ctx.errors.has(TaskError)) {
43
49
  return TASK_ERROR
@@ -50,6 +56,13 @@ export const restoreUnstagedChangesSkipped = (ctx) => {
50
56
  return GIT_ERROR
51
57
  }
52
58
 
59
+ // When complete reverting to original state is skipped,
60
+ // we can still restore unstaged changes to make it easier
61
+ // to do manually.
62
+ if (!ctx.shouldRevert) {
63
+ false
64
+ }
65
+
53
66
  // Should be skipped when tasks fail
54
67
  if (ctx.errors.has(TaskError)) {
55
68
  return TASK_ERROR
@@ -59,17 +72,13 @@ export const restoreUnstagedChangesSkipped = (ctx) => {
59
72
  export const restoreOriginalStateEnabled = (ctx) =>
60
73
  !!ctx.shouldRevert &&
61
74
  !!ctx.shouldBackup &&
62
- (ctx.errors.has(ExitCodeError) ||
75
+ (ctx.errors.has(FailOnChangesError) ||
63
76
  ctx.errors.has(TaskError) ||
64
77
  ctx.errors.has(RestoreUnstagedChangesError))
65
78
 
66
79
  export const restoreOriginalStateSkipped = (ctx) => {
67
80
  // Should be skipped in case of unknown git errors
68
- if (
69
- ctx.errors.has(GitError) &&
70
- !ctx.errors.has(RestoreUnstagedChangesError) &&
71
- !ctx.errors.has(ExitCodeError)
72
- ) {
81
+ if (ctx.errors.has(GitError) && !ctx.errors.has(RestoreUnstagedChangesError)) {
73
82
  return GIT_ERROR
74
83
  }
75
84
  }
@@ -77,6 +86,11 @@ export const restoreOriginalStateSkipped = (ctx) => {
77
86
  export const cleanupEnabled = (ctx) => ctx.shouldBackup
78
87
 
79
88
  export const cleanupSkipped = (ctx) => {
89
+ // "--fail-on-changes" was used, so we shouldn't drop the backup stash
90
+ if (ctx.errors.has(FailOnChangesError) && !ctx.shouldRevert) {
91
+ return true
92
+ }
93
+
80
94
  // Should be skipped in case of unknown git errors
81
95
  if (restoreOriginalStateSkipped(ctx)) {
82
96
  return GIT_ERROR
package/lib/symbols.js CHANGED
@@ -26,4 +26,4 @@ export const RestoreUnstagedChangesError = Symbol('RestoreUnstagedChangesError')
26
26
 
27
27
  export const TaskError = Symbol('TaskError')
28
28
 
29
- export const ExitCodeError = Symbol('ExitCodeError')
29
+ export const FailOnChangesError = Symbol('FailOnChangesError')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "16.2.0",
3
+ "version": "16.2.2",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,25 +48,25 @@
48
48
  "tag": "npx changeset tag"
49
49
  },
50
50
  "dependencies": {
51
- "commander": "14.0.1",
52
- "listr2": "9.0.4",
53
- "micromatch": "4.0.8",
54
- "nano-spawn": "1.0.3",
55
- "pidtree": "0.6.0",
56
- "string-argv": "0.3.2",
57
- "yaml": "2.8.1"
51
+ "commander": "^14.0.1",
52
+ "listr2": "^9.0.4",
53
+ "micromatch": "^4.0.8",
54
+ "nano-spawn": "^1.0.3",
55
+ "pidtree": "^0.6.0",
56
+ "string-argv": "^0.3.2",
57
+ "yaml": "^2.8.1"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@changesets/changelog-github": "0.5.1",
61
61
  "@changesets/cli": "2.29.7",
62
62
  "@commitlint/cli": "19.8.1",
63
63
  "@commitlint/config-conventional": "19.8.1",
64
- "@eslint/js": "9.35.0",
64
+ "@eslint/js": "9.36.0",
65
65
  "@vitest/coverage-v8": "3.2.4",
66
66
  "@vitest/eslint-plugin": "1.3.12",
67
67
  "consolemock": "1.1.0",
68
68
  "cross-env": "10.0.0",
69
- "eslint": "9.35.0",
69
+ "eslint": "9.36.0",
70
70
  "eslint-config-prettier": "10.1.8",
71
71
  "eslint-plugin-n": "17.23.1",
72
72
  "eslint-plugin-prettier": "5.5.4",
@@ -1,3 +0,0 @@
1
- import { pathToFileURL } from 'node:url'
2
-
3
- export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)