lint-staged 16.1.0 → 16.1.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/MIGRATION.md +3 -1
- package/README.md +4 -4
- package/bin/lint-staged.js +1 -1
- package/lib/chunkFiles.js +19 -7
- package/lib/generateTasks.js +31 -14
- package/lib/getFunctionTask.js +2 -2
- package/lib/getSpawnedTask.js +1 -1
- package/lib/getSpawnedTasks.js +5 -3
- package/lib/getStagedFiles.js +32 -2
- package/lib/gitWorkflow.js +25 -3
- package/lib/groupFilesByConfig.js +8 -1
- package/lib/index.js +6 -1
- package/lib/messages.js +7 -15
- package/lib/resolveGitRepo.js +29 -29
- package/lib/runAll.js +23 -13
- package/lib/state.js +5 -4
- package/package.json +7 -7
package/MIGRATION.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
#### Updated Node.js version requirement
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
For version `lint-staged@16.0.0` the lowest supported Node.js version is `20.19.0`, following requirements of `nano-spawn`. Please upgrade your Node.js version.
|
|
6
|
+
|
|
7
|
+
For version `lint-staged@16.1.0` this is lowered to `20.17.0`, again following `nano-spawn`.
|
|
6
8
|
|
|
7
9
|
#### Removed validation for removed advanced configuration file options
|
|
8
10
|
|
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Now change a few files, `git add` or `git add --patch` some of them to your comm
|
|
|
85
85
|
|
|
86
86
|
See [examples](#examples) and [configuration](#configuration) for more information.
|
|
87
87
|
|
|
88
|
-
> [!CAUTION]
|
|
88
|
+
> [!CAUTION]
|
|
89
89
|
> _Lint-staged_ runs `git` operations affecting the files in your repository. By default _lint-staged_ creates a `git stash` as a backup of the original state before running any configured tasks to help prevent data loss.
|
|
90
90
|
|
|
91
91
|
## Changelog
|
|
@@ -143,9 +143,9 @@ Any lost modifications can be restored from a git stash:
|
|
|
143
143
|
- **`--diff`**: By default tasks are filtered against all files staged in git, generated from `git diff --staged`. This option allows you to override the `--staged` flag with arbitrary revisions. For example to get a list of changed files between two branches, use `--diff="branch1...branch2"`. You can also read more from about [git diff](https://git-scm.com/docs/git-diff) and [gitrevisions](https://git-scm.com/docs/gitrevisions). This option also implies `--no-stash`.
|
|
144
144
|
- **`--diff-filter`**: By default only files that are _added_, _copied_, _modified_, or _renamed_ are included. Use this flag to override the default `ACMR` value with something else: _added_ (`A`), _copied_ (`C`), _deleted_ (`D`), _modified_ (`M`), _renamed_ (`R`), _type changed_ (`T`), _unmerged_ (`U`), _unknown_ (`X`), or _pairing broken_ (`B`). See also the `git diff` docs for [--diff-filter](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203).
|
|
145
145
|
- **`--max-arg-length`**: long commands (a lot of files) are automatically split into multiple chunks when it detects the current shell cannot handle them. Use this flag to override the maximum length of the generated command string.
|
|
146
|
-
- **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit.
|
|
147
|
-
- **`--no-hide-partially-staged`**: By default, unstaged changes from partially staged files will be hidden. This option will disable this behavior
|
|
148
|
-
- **`--quiet`**:
|
|
146
|
+
- **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit.
|
|
147
|
+
- **`--no-hide-partially-staged`**: By default, unstaged changes from partially staged files will be hidden and applied back after running tasks. This option will disable this behavior, causing those changes to also be committed.
|
|
148
|
+
- **`--quiet`**: Suppress all CLI output, except from tasks.
|
|
149
149
|
- **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
|
|
150
150
|
- **`--no-revert`**: By default all task modifications will be reverted in case of an error. This option will disable the behavior, and apply task modifications to the index before aborting the commit.
|
|
151
151
|
- **`--verbose`**: Show task output even when tasks succeed. By default only failed output is shown.
|
package/bin/lint-staged.js
CHANGED
package/lib/chunkFiles.js
CHANGED
|
@@ -10,7 +10,7 @@ const debugLog = debug('lint-staged:chunkFiles')
|
|
|
10
10
|
* Chunk array into sub-arrays
|
|
11
11
|
* @param {Array} arr
|
|
12
12
|
* @param {Number} chunkCount
|
|
13
|
-
* @
|
|
13
|
+
* @returns {Array<Array>}
|
|
14
14
|
*/
|
|
15
15
|
const chunkArray = (arr, chunkCount) => {
|
|
16
16
|
if (chunkCount === 1) return [arr]
|
|
@@ -27,24 +27,36 @@ const chunkArray = (arr, chunkCount) => {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Chunk files into sub-arrays based on the length of the resulting argument string
|
|
30
|
+
*
|
|
31
|
+
* @typedef {import('./getStagedFiles.js').StagedFile[]} StagedFile
|
|
32
|
+
*
|
|
30
33
|
* @param {Object} opts
|
|
31
|
-
* @param {Array<
|
|
34
|
+
* @param {Array<StagedFile>} opts.files
|
|
32
35
|
* @param {String} [opts.baseDir] The optional base directory to resolve relative paths.
|
|
33
36
|
* @param {number} [opts.maxArgLength] the maximum argument string length
|
|
34
37
|
* @param {Boolean} [opts.relative] whether files are relative to `topLevelDir` or should be resolved as absolute
|
|
35
|
-
* @returns {Array<Array<
|
|
38
|
+
* @returns {Array<Array<StagedFile>>}
|
|
36
39
|
*/
|
|
37
40
|
export const chunkFiles = ({ files, baseDir, maxArgLength = null, relative = false }) => {
|
|
38
|
-
const normalizedFiles = files.map((file) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
const normalizedFiles = files.map((file) => {
|
|
42
|
+
return {
|
|
43
|
+
filepath: normalizePath(
|
|
44
|
+
relative || !baseDir ? file.filepath : path.resolve(baseDir, file.filepath)
|
|
45
|
+
),
|
|
46
|
+
status: file.status,
|
|
47
|
+
}
|
|
48
|
+
})
|
|
41
49
|
|
|
42
50
|
if (!maxArgLength) {
|
|
43
51
|
debugLog('Skip chunking files because of undefined maxArgLength')
|
|
44
52
|
return [normalizedFiles] // wrap in an array to return a single chunk
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
/** Calculate total character length of all filepaths, with added spaces in between */
|
|
56
|
+
const fileListLength =
|
|
57
|
+
normalizedFiles.reduce((sum, file) => sum + file.filepath.length, 0) +
|
|
58
|
+
Math.max(normalizedFiles.length - 1, 0)
|
|
59
|
+
|
|
48
60
|
debugLog(
|
|
49
61
|
`Resolved an argument string length of ${fileListLength} characters from ${normalizedFiles.length} files`
|
|
50
62
|
)
|
package/lib/generateTasks.js
CHANGED
|
@@ -13,13 +13,17 @@ const debugLog = debug('lint-staged:generateTasks')
|
|
|
13
13
|
* @param {object} options
|
|
14
14
|
* @param {Object} [options.config] - Task configuration
|
|
15
15
|
* @param {Object} [options.cwd] - Current working directory
|
|
16
|
-
* @param {
|
|
16
|
+
* @param {import('./getStagedFiles.js').StagedFile[]} [options.files] - Staged filepaths
|
|
17
17
|
* @param {boolean} [options.relative] - Whether filepaths to should be relative to cwd
|
|
18
18
|
*/
|
|
19
19
|
export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => {
|
|
20
20
|
debugLog('Generating linter tasks')
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/** @type {StagedFile[]} */
|
|
23
|
+
const relativeFiles = files.map((file) => ({
|
|
24
|
+
filepath: normalizePath(path.relative(cwd, file.filepath)),
|
|
25
|
+
status: file.status,
|
|
26
|
+
}))
|
|
23
27
|
|
|
24
28
|
return Object.entries(config).map(([pattern, commands]) => {
|
|
25
29
|
const isParentDirPattern = pattern.startsWith('../')
|
|
@@ -28,21 +32,34 @@ export const generateTasks = ({ config, cwd = process.cwd(), files, relative = f
|
|
|
28
32
|
// specifies that it concerns a parent directory.
|
|
29
33
|
const filteredFiles = relativeFiles.filter((file) => {
|
|
30
34
|
if (isParentDirPattern) return true
|
|
31
|
-
return !file.startsWith('..') && !path.isAbsolute(file)
|
|
35
|
+
return !file.filepath.startsWith('..') && !path.isAbsolute(file.filepath)
|
|
32
36
|
})
|
|
33
37
|
|
|
34
|
-
const matches = micromatch(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const matches = micromatch(
|
|
39
|
+
filteredFiles.map((file) => file.filepath),
|
|
40
|
+
pattern,
|
|
41
|
+
{
|
|
42
|
+
cwd,
|
|
43
|
+
dot: true,
|
|
44
|
+
// If the pattern doesn't look like a path, enable `matchBase` to
|
|
45
|
+
// match against filenames in every directory. This makes `*.js`
|
|
46
|
+
// match both `test.js` and `subdirectory/test.js`.
|
|
47
|
+
matchBase: !pattern.includes('/'),
|
|
48
|
+
posixSlashes: true,
|
|
49
|
+
strictBrackets: true,
|
|
50
|
+
}
|
|
51
|
+
)
|
|
44
52
|
|
|
45
|
-
const fileList =
|
|
53
|
+
const fileList = filteredFiles.flatMap((file) =>
|
|
54
|
+
matches.includes(file.filepath)
|
|
55
|
+
? [
|
|
56
|
+
{
|
|
57
|
+
filepath: normalizePath(relative ? file.filepath : path.resolve(cwd, file.filepath)),
|
|
58
|
+
status: file.status,
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
: []
|
|
62
|
+
)
|
|
46
63
|
|
|
47
64
|
const task = { pattern, commands, fileList }
|
|
48
65
|
debugLog('Generated task: \n%O', task)
|
package/lib/getFunctionTask.js
CHANGED
|
@@ -15,7 +15,7 @@ export const isFunctionTask = (commands) => typeof commands === 'object' && !Arr
|
|
|
15
15
|
* Handles function configuration and pushes the tasks into the task array
|
|
16
16
|
*
|
|
17
17
|
* @param {object} command
|
|
18
|
-
* @param {
|
|
18
|
+
* @param {import('./getStagedFiles.js').StagedFile[]} files
|
|
19
19
|
* @throws {Error} If the function configuration is not valid
|
|
20
20
|
*/
|
|
21
21
|
export const getFunctionTask = async (command, files) => {
|
|
@@ -23,7 +23,7 @@ export const getFunctionTask = async (command, files) => {
|
|
|
23
23
|
|
|
24
24
|
const task = async (ctx) => {
|
|
25
25
|
try {
|
|
26
|
-
await command.task(files)
|
|
26
|
+
await command.task(files.map((file) => file.filepath))
|
|
27
27
|
} catch (e) {
|
|
28
28
|
throw makeErr(command.title, e, ctx)
|
|
29
29
|
}
|
package/lib/getSpawnedTask.js
CHANGED
|
@@ -116,7 +116,7 @@ export const makeErr = (command, error, ctx) => {
|
|
|
116
116
|
* @param {string} [options.cwd]
|
|
117
117
|
* @param {String} options.topLevelDir - Current git repo top-level path
|
|
118
118
|
* @param {Boolean} options.isFn - Whether the linter task is a function
|
|
119
|
-
* @param {
|
|
119
|
+
* @param {string[]} options.files — Filepaths to run the linter task against
|
|
120
120
|
* @param {Boolean} [options.verbose] — Always show task verbose
|
|
121
121
|
* @returns {() => Promise<Array<string>>}
|
|
122
122
|
*/
|
package/lib/getSpawnedTasks.js
CHANGED
|
@@ -11,7 +11,7 @@ const debugLog = debug('lint-staged:getSpawnedTasks')
|
|
|
11
11
|
* @param {object} options
|
|
12
12
|
* @param {Array<string|Function>|string|Function} options.commands
|
|
13
13
|
* @param {string} options.cwd
|
|
14
|
-
* @param {
|
|
14
|
+
* @param {import('./getStagedFiles.js').StagedFile[]} options.files
|
|
15
15
|
* @param {string} options.topLevelDir
|
|
16
16
|
* @param {Boolean} verbose
|
|
17
17
|
*/
|
|
@@ -21,12 +21,14 @@ export const getSpawnedTasks = async ({ commands, cwd, files, topLevelDir, verbo
|
|
|
21
21
|
|
|
22
22
|
const commandArray = Array.isArray(commands) ? commands : [commands]
|
|
23
23
|
|
|
24
|
+
const filepaths = files.map((f) => f.filepath)
|
|
25
|
+
|
|
24
26
|
for (const cmd of commandArray) {
|
|
25
27
|
// command function may return array of commands that already include `stagedFiles`
|
|
26
28
|
const isFn = typeof cmd === 'function'
|
|
27
29
|
|
|
28
30
|
/** Pass copy of file list to prevent mutation by function from config file. */
|
|
29
|
-
const resolved = isFn ? await cmd([...
|
|
31
|
+
const resolved = isFn ? await cmd([...filepaths]) : cmd
|
|
30
32
|
|
|
31
33
|
const resolvedArray = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array
|
|
32
34
|
|
|
@@ -43,7 +45,7 @@ export const getSpawnedTasks = async ({ commands, cwd, files, topLevelDir, verbo
|
|
|
43
45
|
)
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
const task = getSpawnedTask({ command, cwd, files, topLevelDir, isFn, verbose })
|
|
48
|
+
const task = getSpawnedTask({ command, cwd, files: filepaths, topLevelDir, isFn, verbose })
|
|
47
49
|
cmdTasks.push({ title: command, command, task })
|
|
48
50
|
}
|
|
49
51
|
}
|
package/lib/getStagedFiles.js
CHANGED
|
@@ -5,6 +5,16 @@ import { getDiffCommand } from './getDiffCommand.js'
|
|
|
5
5
|
import { normalizePath } from './normalizePath.js'
|
|
6
6
|
import { parseGitZOutput } from './parseGitZOutput.js'
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {'A'|'C'|'D'|'M'|'R'|'T'|'U'|'X'} FileSatus
|
|
10
|
+
* @typedef { { filepath: string; status: FileSatus }} StagedFile
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} args
|
|
13
|
+
* @param {string} [args.cwd]
|
|
14
|
+
* @param {string} [args.diff]
|
|
15
|
+
* @param {string} [args.diffFilter]
|
|
16
|
+
* @retuns {Promise<StagedFile[] | null>}
|
|
17
|
+
*/
|
|
8
18
|
export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } = {}) => {
|
|
9
19
|
try {
|
|
10
20
|
/**
|
|
@@ -35,7 +45,7 @@ export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } =
|
|
|
35
45
|
.split('\u0000:')
|
|
36
46
|
.map(parseGitZOutput)
|
|
37
47
|
.flatMap(([info, src, dst]) => {
|
|
38
|
-
const [, dstMode, , ,
|
|
48
|
+
const [, dstMode, , , statusWithScore] = info.split(' ')
|
|
39
49
|
|
|
40
50
|
/**
|
|
41
51
|
* Filter out submodules and symlinks
|
|
@@ -45,10 +55,30 @@ export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } =
|
|
|
45
55
|
return []
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @example "M"
|
|
60
|
+
* @example "R86"
|
|
61
|
+
*
|
|
62
|
+
* - A: addition of a file
|
|
63
|
+
* - C: copy of a file into a new one
|
|
64
|
+
* - D: deletion of a file
|
|
65
|
+
* - M: modification of the contents or mode of a file
|
|
66
|
+
* - R: renaming of a file
|
|
67
|
+
* - T: change in the type of the file (regular file, symbolic link or submodule)
|
|
68
|
+
* - U: file is unmerged (you must complete the merge before it can be committed)
|
|
69
|
+
* - X: "unknown" change type (most probably a bug, please report it)
|
|
70
|
+
*/
|
|
71
|
+
const status = statusWithScore[0]
|
|
72
|
+
|
|
48
73
|
/** "dst" exists when moving files, otherwise it's undefined and only "src" exists */
|
|
49
74
|
const filename = dst ?? src
|
|
50
75
|
|
|
51
|
-
return [
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
filepath: normalizePath(path.resolve(cwd, filename)),
|
|
79
|
+
status,
|
|
80
|
+
},
|
|
81
|
+
]
|
|
52
82
|
})
|
|
53
83
|
} catch {
|
|
54
84
|
return null
|
package/lib/gitWorkflow.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
|
|
3
4
|
import debug from 'debug'
|
|
@@ -66,6 +67,10 @@ const handleError = (error, ctx, symbol) => {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export class GitWorkflow {
|
|
70
|
+
/**
|
|
71
|
+
* @param {Object} opts
|
|
72
|
+
* @param {import('./getStagedFiles.js').StagedFile[][]} opts.matchedFileChunks
|
|
73
|
+
*/
|
|
69
74
|
constructor({ allowEmpty, gitConfigDir, topLevelDir, matchedFileChunks, diff, diffFilter }) {
|
|
70
75
|
this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: topLevelDir })
|
|
71
76
|
this.deletedFiles = []
|
|
@@ -74,6 +79,7 @@ export class GitWorkflow {
|
|
|
74
79
|
this.diff = diff
|
|
75
80
|
this.diffFilter = diffFilter
|
|
76
81
|
this.allowEmpty = allowEmpty
|
|
82
|
+
/** @type {import('./getStagedFiles.js').StagedFile[][]} */
|
|
77
83
|
this.matchedFileChunks = matchedFileChunks
|
|
78
84
|
|
|
79
85
|
/**
|
|
@@ -185,7 +191,7 @@ export class GitWorkflow {
|
|
|
185
191
|
const [index, workingTree] = line
|
|
186
192
|
return index !== ' ' && workingTree !== ' ' && index !== '?' && workingTree !== '?'
|
|
187
193
|
})
|
|
188
|
-
.map((line) => line.
|
|
194
|
+
.map((line) => line.slice(3)) // Remove first three letters (index, workingTree, and a whitespace)
|
|
189
195
|
.filter(Boolean) // Filter empty string
|
|
190
196
|
debugLog('Found partially staged files:', partiallyStaged)
|
|
191
197
|
return partiallyStaged.length ? partiallyStaged : null
|
|
@@ -198,13 +204,14 @@ export class GitWorkflow {
|
|
|
198
204
|
try {
|
|
199
205
|
debugLog(task.title)
|
|
200
206
|
|
|
201
|
-
// Get a list of files with
|
|
207
|
+
// Get a list of files with both staged and unstaged changes.
|
|
202
208
|
// Unstaged changes to these files should be hidden before the tasks run.
|
|
203
209
|
this.partiallyStagedFiles = await this.getPartiallyStagedFiles()
|
|
204
210
|
|
|
205
211
|
if (this.partiallyStagedFiles) {
|
|
206
212
|
ctx.hasPartiallyStagedFiles = true
|
|
207
213
|
const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
|
|
214
|
+
ctx.unstagedPatch = unstagedPatch
|
|
208
215
|
const files = processRenames(this.partiallyStagedFiles)
|
|
209
216
|
await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
|
|
210
217
|
} else {
|
|
@@ -273,7 +280,22 @@ export class GitWorkflow {
|
|
|
273
280
|
// These additions per chunk are run "serially" to prevent race conditions.
|
|
274
281
|
// Git add creates a lockfile in the repo causing concurrent operations to fail.
|
|
275
282
|
for (const files of this.matchedFileChunks) {
|
|
276
|
-
await
|
|
283
|
+
const accessCheckedFiles = await Promise.allSettled(
|
|
284
|
+
files.map(async (f) => {
|
|
285
|
+
if (f.status === 'D') {
|
|
286
|
+
await fs.access(f.filepath)
|
|
287
|
+
return f.filepath // File is no longer deleted and can be added
|
|
288
|
+
} else {
|
|
289
|
+
return f.filepath
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
const addableFiles = accessCheckedFiles.flatMap((r) =>
|
|
295
|
+
r.status === 'fulfilled' ? [r.value] : []
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
await this.execGit(['add', '--', ...addableFiles])
|
|
277
299
|
}
|
|
278
300
|
|
|
279
301
|
debugLog('Done adding task modifications to index!')
|
|
@@ -4,10 +4,17 @@ import debug from 'debug'
|
|
|
4
4
|
|
|
5
5
|
const debugLog = debug('lint-staged:groupFilesByConfig')
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('./getStagedFiles.js').StagedFile} StagedFile
|
|
9
|
+
* @type {(args: { config: {[key: string]: { config: any; files: string[] }}; files: StagedFile[]; singleConfigMode?: boolean }) => Promise<{[key: string]: { config: any; files: StagedFile[] } }>
|
|
10
|
+
*/
|
|
7
11
|
export const groupFilesByConfig = async ({ configs, files, singleConfigMode }) => {
|
|
8
12
|
debugLog('Grouping %d files by %d configurations', files.length, Object.keys(configs).length)
|
|
9
13
|
|
|
14
|
+
/** @type {Set<StagedFile>} */
|
|
10
15
|
const filesSet = new Set(files)
|
|
16
|
+
|
|
17
|
+
/** @type {{[key: string]: { config: any; files: StagedFile[] } }} */
|
|
11
18
|
const filesByConfig = {}
|
|
12
19
|
|
|
13
20
|
/** Configs are sorted deepest first by `searchConfigs` */
|
|
@@ -22,7 +29,7 @@ export const groupFilesByConfig = async ({ configs, files, singleConfigMode }) =
|
|
|
22
29
|
|
|
23
30
|
/** Check if file is inside directory of the configuration file */
|
|
24
31
|
const isInsideDir = (file) => {
|
|
25
|
-
const relative = path.relative(dir, file)
|
|
32
|
+
const relative = path.relative(dir, file.filepath)
|
|
26
33
|
return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
|
|
27
34
|
}
|
|
28
35
|
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
NO_CONFIGURATION,
|
|
7
7
|
PREVENTED_EMPTY_COMMIT,
|
|
8
8
|
RESTORE_STASH_EXAMPLE,
|
|
9
|
+
UNSTAGED_CHANGES_BACKUP_STASH_LOCATION,
|
|
9
10
|
} from './messages.js'
|
|
10
11
|
import { printTaskOutput } from './printTaskOutput.js'
|
|
11
12
|
import { runAll } from './runAll.js'
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
ConfigNotFoundError,
|
|
16
17
|
GetBackupStashError,
|
|
17
18
|
GitError,
|
|
19
|
+
RestoreUnstagedChangesError,
|
|
18
20
|
} from './symbols.js'
|
|
19
21
|
import { validateOptions } from './validateOptions.js'
|
|
20
22
|
import { getVersion } from './version.js'
|
|
@@ -81,7 +83,7 @@ const lintStaged = async (
|
|
|
81
83
|
stash = diff === undefined,
|
|
82
84
|
// Cannot revert to original state without stash
|
|
83
85
|
revert = stash,
|
|
84
|
-
hidePartiallyStaged =
|
|
86
|
+
hidePartiallyStaged = true,
|
|
85
87
|
verbose = false,
|
|
86
88
|
} = {},
|
|
87
89
|
logger = console
|
|
@@ -138,6 +140,9 @@ const lintStaged = async (
|
|
|
138
140
|
logger.error(NO_CONFIGURATION)
|
|
139
141
|
} else if (ctx.errors.has(ApplyEmptyCommitError)) {
|
|
140
142
|
logger.warn(PREVENTED_EMPTY_COMMIT)
|
|
143
|
+
} else if (ctx.errors.has(RestoreUnstagedChangesError)) {
|
|
144
|
+
logger.warn(UNSTAGED_CHANGES_BACKUP_STASH_LOCATION)
|
|
145
|
+
logger.warn(ctx.unstagedPatch)
|
|
141
146
|
} else if (
|
|
142
147
|
(ctx.errors.has(GitError) || cleanupSkipped(ctx)) &&
|
|
143
148
|
!ctx.errors.has(GetBackupStashError)
|
package/lib/messages.js
CHANGED
|
@@ -31,25 +31,15 @@ export const skippingBackup = (hasInitialCommit, diff) => {
|
|
|
31
31
|
const reason =
|
|
32
32
|
diff !== undefined
|
|
33
33
|
? '`--diff` was used'
|
|
34
|
-
: hasInitialCommit
|
|
35
|
-
|
|
36
|
-
: 'there’s no initial commit yet'
|
|
34
|
+
: (hasInitialCommit ? '`--no-stash` was used' : 'there’s no initial commit yet') +
|
|
35
|
+
'. This might result in data loss'
|
|
37
36
|
|
|
38
37
|
return chalk.yellow(`${warning} Skipping backup because ${reason}.\n`)
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
export const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
? '`--diff` was used'
|
|
45
|
-
: !stash
|
|
46
|
-
? '`--no-stash` was used'
|
|
47
|
-
: '`--no-hide-partially-staged` was used'
|
|
48
|
-
|
|
49
|
-
return chalk.yellow(
|
|
50
|
-
`${warning} Skipping hiding unstaged changes from partially staged files because ${reason}.\n`
|
|
51
|
-
)
|
|
52
|
-
}
|
|
40
|
+
export const SKIPPING_HIDE_PARTIALLY_CHANGED = chalk.yellow(
|
|
41
|
+
`${warning} Skipping hiding unstaged changes from partially staged files because \`--no-hide-partially-staged\` was used.\n`
|
|
42
|
+
)
|
|
53
43
|
|
|
54
44
|
export const DEPRECATED_GIT_ADD = chalk.yellow(
|
|
55
45
|
`${warning} 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.
|
|
@@ -96,3 +86,5 @@ export const failedToParseConfig = (
|
|
|
96
86
|
${error}
|
|
97
87
|
|
|
98
88
|
See https://github.com/okonet/lint-staged#configuration.`
|
|
89
|
+
|
|
90
|
+
export const UNSTAGED_CHANGES_BACKUP_STASH_LOCATION = `Unstaged changes have been kept back in a patch file:`
|
package/lib/resolveGitRepo.js
CHANGED
|
@@ -8,53 +8,53 @@ import { normalizePath } from './normalizePath.js'
|
|
|
8
8
|
const debugLog = debug('lint-staged:resolveGitRepo')
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @example ".git"
|
|
11
|
+
* Relative path up to the repo top-level directory
|
|
12
|
+
* @example "../"
|
|
14
13
|
*/
|
|
15
|
-
const
|
|
16
|
-
/**
|
|
17
|
-
* Absolute repo top-level directory
|
|
18
|
-
*
|
|
19
|
-
* @example <caption>Git on macOS</caption>
|
|
20
|
-
* "/Users/iiro/Documents/git/lint-staged"
|
|
21
|
-
*
|
|
22
|
-
* @example <caption>Git for Windows</caption>
|
|
23
|
-
* "C:\Users\iiro\Documents\git\lint-staged"
|
|
24
|
-
*
|
|
25
|
-
* @example <caption>Git installed with MSYS2, this doesn't work when used as CWD with Node.js child_process</caption>
|
|
26
|
-
* "/c/Users/iiro/Documents/git/lint-staged"
|
|
27
|
-
*/
|
|
28
|
-
const topLevelPromise = execGit(['rev-parse', '--show-toplevel'], { cwd })
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Absolute .git directory, similar to top-level
|
|
32
|
-
*
|
|
33
|
-
* @example "/Users/iiro/Documents/git/lint-staged/.git"
|
|
34
|
-
*/
|
|
35
|
-
const absoluteGitDirPromise = execGit(['rev-parse', '--absolute-git-dir'], { cwd })
|
|
14
|
+
const CDUP = '--show-cdup'
|
|
36
15
|
|
|
37
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Absolute repo top-level directory
|
|
18
|
+
*
|
|
19
|
+
* @example <caption>Git on macOS</caption>
|
|
20
|
+
* "/Users/iiro/Documents/git/lint-staged"
|
|
21
|
+
*
|
|
22
|
+
* @example <caption>Git for Windows</caption>
|
|
23
|
+
* "C:\Users\iiro\Documents\git\lint-staged"
|
|
24
|
+
*
|
|
25
|
+
* @example <caption>Git installed with MSYS2, this doesn't work when used as CWD with Node.js child_process</caption>
|
|
26
|
+
* "/c/Users/iiro/Documents/git/lint-staged"
|
|
27
|
+
*/
|
|
28
|
+
const TOPLEVEL = '--show-toplevel'
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Absolute .git directory, similar to top-level
|
|
32
|
+
*
|
|
33
|
+
* @example "/Users/iiro/Documents/git/lint-staged/.git"
|
|
34
|
+
*/
|
|
35
|
+
const ABSOLUTE_GIT_DIR = '--absolute-git-dir'
|
|
41
36
|
|
|
42
37
|
/** Resolve git directory and possible submodule paths */
|
|
43
38
|
export const resolveGitRepo = async (cwd = process.cwd()) => {
|
|
44
39
|
try {
|
|
45
40
|
debugLog('Resolving git repo from `%s`', cwd)
|
|
46
41
|
|
|
42
|
+
/** Git rev-parse returns all three flag values on separate lines */
|
|
43
|
+
const revParseOutput = await execGit(['rev-parse', CDUP, TOPLEVEL, ABSOLUTE_GIT_DIR], {
|
|
44
|
+
cwd,
|
|
45
|
+
})
|
|
46
|
+
const [relativeTopLevelDir, topLevel, absoluteGitDir] = revParseOutput.split('\n')
|
|
47
|
+
|
|
47
48
|
// Unset GIT_DIR before running any git operations in case it's pointing to an incorrect location
|
|
48
49
|
debugLog('Unset GIT_DIR (was `%s`)', process.env.GIT_DIR)
|
|
49
50
|
delete process.env.GIT_DIR
|
|
50
51
|
debugLog('Unset GIT_WORK_TREE (was `%s`)', process.env.GIT_WORK_TREE)
|
|
51
52
|
delete process.env.GIT_WORK_TREE
|
|
52
53
|
|
|
53
|
-
const relativeTopLevelDir = await execGit(['rev-parse', '--show-cdup'], { cwd })
|
|
54
54
|
const topLevelDir = normalizePath(path.join(cwd, relativeTopLevelDir))
|
|
55
55
|
debugLog('Resolved git repository top-level directory to be `%s`', topLevelDir)
|
|
56
56
|
|
|
57
|
-
const relativeGitConfigDir =
|
|
57
|
+
const relativeGitConfigDir = path.relative(topLevel, absoluteGitDir)
|
|
58
58
|
const gitConfigDir = normalizePath(path.join(topLevelDir, relativeGitConfigDir))
|
|
59
59
|
debugLog('Resolved git config directory to be `%s`', gitConfigDir)
|
|
60
60
|
|
package/lib/runAll.js
CHANGED
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
NO_TASKS,
|
|
23
23
|
NOT_GIT_REPO,
|
|
24
24
|
SKIPPED_GIT_ERROR,
|
|
25
|
+
SKIPPING_HIDE_PARTIALLY_CHANGED,
|
|
25
26
|
skippingBackup,
|
|
26
|
-
skippingHidePartiallyStaged,
|
|
27
27
|
} from './messages.js'
|
|
28
28
|
import { normalizePath } from './normalizePath.js'
|
|
29
29
|
import { resolveGitRepo } from './resolveGitRepo.js'
|
|
@@ -42,7 +42,12 @@ import { ConfigNotFoundError, GetStagedFilesError, GitError, GitRepoError } from
|
|
|
42
42
|
|
|
43
43
|
const debugLog = debug('lint-staged:runAll')
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* @param {ReturnType<typeof getInitialState>} ctx context
|
|
47
|
+
* @param {unknown} cause error cause
|
|
48
|
+
*/
|
|
49
|
+
const createError = (ctx, cause) =>
|
|
50
|
+
Object.assign(new Error('lint-staged failed', { cause }), { ctx })
|
|
46
51
|
|
|
47
52
|
/**
|
|
48
53
|
* Executes all tasks and either resolves or rejects the promise
|
|
@@ -82,7 +87,7 @@ export const runAll = async (
|
|
|
82
87
|
stash = diff === undefined,
|
|
83
88
|
// Cannot revert to original state without stash
|
|
84
89
|
revert = stash,
|
|
85
|
-
hidePartiallyStaged =
|
|
90
|
+
hidePartiallyStaged = true,
|
|
86
91
|
verbose = false,
|
|
87
92
|
},
|
|
88
93
|
logger = console
|
|
@@ -100,7 +105,7 @@ export const runAll = async (
|
|
|
100
105
|
if (!topLevelDir) {
|
|
101
106
|
if (!quiet) ctx.output.push(NOT_GIT_REPO)
|
|
102
107
|
ctx.errors.add(GitRepoError)
|
|
103
|
-
throw createError(ctx)
|
|
108
|
+
throw createError(ctx, GitRepoError)
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
// Test whether we have any commits or not.
|
|
@@ -118,19 +123,19 @@ export const runAll = async (
|
|
|
118
123
|
|
|
119
124
|
ctx.shouldHidePartiallyStaged = hidePartiallyStaged
|
|
120
125
|
if (!ctx.shouldHidePartiallyStaged && !quiet) {
|
|
121
|
-
logger.warn(
|
|
126
|
+
logger.warn(SKIPPING_HIDE_PARTIALLY_CHANGED)
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
129
|
+
const stagedFiles = await getStagedFiles({ cwd: topLevelDir, diff, diffFilter })
|
|
130
|
+
if (!stagedFiles) {
|
|
126
131
|
if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
|
|
127
132
|
ctx.errors.add(GetStagedFilesError)
|
|
128
133
|
throw createError(ctx, GetStagedFilesError)
|
|
129
134
|
}
|
|
130
|
-
debugLog('Loaded list of staged files in git:\n%O',
|
|
135
|
+
debugLog('Loaded list of staged files in git:\n%O', stagedFiles)
|
|
131
136
|
|
|
132
137
|
// If there are no files avoid executing any lint-staged logic
|
|
133
|
-
if (
|
|
138
|
+
if (stagedFiles.length === 0) {
|
|
134
139
|
if (!quiet) ctx.output.push(NO_STAGED_FILES)
|
|
135
140
|
return ctx
|
|
136
141
|
}
|
|
@@ -146,7 +151,7 @@ export const runAll = async (
|
|
|
146
151
|
|
|
147
152
|
const filesByConfig = await groupFilesByConfig({
|
|
148
153
|
configs: foundConfigs,
|
|
149
|
-
files,
|
|
154
|
+
files: stagedFiles,
|
|
150
155
|
singleConfigMode: configObject || configPath !== undefined,
|
|
151
156
|
})
|
|
152
157
|
|
|
@@ -174,6 +179,7 @@ export const runAll = async (
|
|
|
174
179
|
const listrTasks = []
|
|
175
180
|
|
|
176
181
|
// Set of all staged files that matched a task glob. Values in a set are unique.
|
|
182
|
+
/** @type {Set<import('./getStagedFiles.js').StagedFile>} */
|
|
177
183
|
const matchedFiles = new Set()
|
|
178
184
|
|
|
179
185
|
for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
|
|
@@ -195,7 +201,7 @@ export const runAll = async (
|
|
|
195
201
|
const chunkListrTasks = await Promise.all(
|
|
196
202
|
generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
|
|
197
203
|
(isFunctionTask(task.commands)
|
|
198
|
-
? getFunctionTask(task.commands,
|
|
204
|
+
? getFunctionTask(task.commands, task.fileList)
|
|
199
205
|
: getSpawnedTasks({
|
|
200
206
|
commands: task.commands,
|
|
201
207
|
cwd: groupCwd,
|
|
@@ -209,9 +215,12 @@ export const runAll = async (
|
|
|
209
215
|
// Make sure relative files are normalized to the
|
|
210
216
|
// group cwd, because other there might be identical
|
|
211
217
|
// relative filenames in the entire set.
|
|
212
|
-
const normalizedFile = path.isAbsolute(file)
|
|
218
|
+
const normalizedFile = path.isAbsolute(file.filepath)
|
|
213
219
|
? file
|
|
214
|
-
:
|
|
220
|
+
: {
|
|
221
|
+
filepath: normalizePath(path.join(groupCwd, file.filepath)),
|
|
222
|
+
status: file.status,
|
|
223
|
+
}
|
|
215
224
|
|
|
216
225
|
matchedFiles.add(normalizedFile)
|
|
217
226
|
})
|
|
@@ -275,6 +284,7 @@ export const runAll = async (
|
|
|
275
284
|
}
|
|
276
285
|
|
|
277
286
|
// Chunk matched files for better Windows compatibility
|
|
287
|
+
/** @type {import('./getStagedFiles.js').StagedFile[][]} */
|
|
278
288
|
const matchedFileChunks = chunkFiles({
|
|
279
289
|
// matched files are relative to `cwd`, not `topLevelDir`, when `relative` is used
|
|
280
290
|
baseDir: cwd,
|
package/lib/state.js
CHANGED
|
@@ -9,15 +9,16 @@ import {
|
|
|
9
9
|
} from './symbols.js'
|
|
10
10
|
|
|
11
11
|
export const getInitialState = ({ quiet = false, revert = true } = {}) => ({
|
|
12
|
-
hasPartiallyStagedFiles: null,
|
|
13
|
-
shouldBackup: null,
|
|
14
|
-
shouldRevert: revert,
|
|
15
12
|
backupHash: null,
|
|
16
|
-
shouldHidePartiallyStaged: true,
|
|
17
13
|
errors: new Set([]),
|
|
18
14
|
events: new EventEmitter(),
|
|
15
|
+
hasPartiallyStagedFiles: null,
|
|
19
16
|
output: [],
|
|
20
17
|
quiet,
|
|
18
|
+
shouldBackup: null,
|
|
19
|
+
shouldHidePartiallyStaged: true,
|
|
20
|
+
shouldRevert: revert,
|
|
21
|
+
unstagedPatch: null,
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
export const shouldHidePartiallyStagedFiles = (ctx) =>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lint-staged",
|
|
3
|
-
"version": "16.1.
|
|
3
|
+
"version": "16.1.2",
|
|
4
4
|
"description": "Lint files staged by git",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -63,17 +63,17 @@
|
|
|
63
63
|
"@changesets/cli": "2.29.4",
|
|
64
64
|
"@commitlint/cli": "19.8.1",
|
|
65
65
|
"@commitlint/config-conventional": "19.8.1",
|
|
66
|
-
"@eslint/js": "9.
|
|
66
|
+
"@eslint/js": "9.29.0",
|
|
67
67
|
"consolemock": "1.1.0",
|
|
68
68
|
"cross-env": "7.0.3",
|
|
69
|
-
"eslint": "9.
|
|
69
|
+
"eslint": "9.29.0",
|
|
70
70
|
"eslint-config-prettier": "10.1.5",
|
|
71
|
-
"eslint-plugin-jest": "28.
|
|
72
|
-
"eslint-plugin-n": "17.
|
|
73
|
-
"eslint-plugin-prettier": "5.4.
|
|
71
|
+
"eslint-plugin-jest": "28.13.5",
|
|
72
|
+
"eslint-plugin-n": "17.20.0",
|
|
73
|
+
"eslint-plugin-prettier": "5.4.1",
|
|
74
74
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
|
75
75
|
"husky": "9.1.7",
|
|
76
|
-
"jest": "
|
|
76
|
+
"jest": "30.0.0",
|
|
77
77
|
"jest-snapshot-serializer-ansi": "2.2.1",
|
|
78
78
|
"mock-stdin": "1.0.0",
|
|
79
79
|
"prettier": "3.5.3",
|