lint-staged 16.4.0 → 17.0.0
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 +20 -0
- package/README.md +35 -31
- package/bin/lint-staged.js +16 -135
- package/lib/assertGitVersion.js +48 -0
- package/lib/cli.js +242 -0
- package/lib/colors.js +8 -103
- package/lib/debug.js +3 -5
- package/lib/getRenderer.js +1 -1
- package/lib/getSpawnedTask.js +2 -2
- package/lib/gitWorkflow.js +55 -68
- package/lib/index.d.ts +5 -0
- package/lib/index.js +9 -1
- package/lib/messages.js +8 -2
- package/lib/runAll.js +23 -25
- package/lib/state.js +33 -17
- package/package.json +19 -19
package/lib/colors.js
CHANGED
|
@@ -1,106 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable n/no-unsupported-features/node-builtins */
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* @example NO_COLOR
|
|
5
|
-
* @example NO_COLOR=1
|
|
6
|
-
* @example NO_COLOR=true
|
|
7
|
-
*/
|
|
8
|
-
const TRUTHRY_ENV_VAR_VALUES = ['', '1', 'true']
|
|
3
|
+
import util from 'node:util'
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
* @example FORCE_COLOR=0
|
|
12
|
-
* @example FORCE_COLOR=false
|
|
13
|
-
*/
|
|
14
|
-
const FALSY_ENV_VAR_VALUES = ['0', 'false']
|
|
5
|
+
export const SUPPORTS_COLOR = !!process.stdout.hasColors?.()
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
export const supportsAnsiColors = (p = process, isTty = nodeTty.isatty(1)) => {
|
|
23
|
-
const noColor = p?.env?.NO_COLOR?.toLowerCase()
|
|
24
|
-
if (TRUTHRY_ENV_VAR_VALUES.includes(noColor)) {
|
|
25
|
-
return false
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const forceColor = p?.env?.FORCE_COLOR?.toLowerCase()
|
|
29
|
-
if (TRUTHRY_ENV_VAR_VALUES.includes(forceColor)) {
|
|
30
|
-
return true
|
|
31
|
-
} else if (FALSY_ENV_VAR_VALUES.includes(forceColor)) {
|
|
32
|
-
return false
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const forceTty = p?.env?.FORCE_TTY
|
|
36
|
-
if (TRUTHRY_ENV_VAR_VALUES.includes(forceTty)) {
|
|
37
|
-
return true
|
|
38
|
-
} else if (FALSY_ENV_VAR_VALUES.includes(forceTty)) {
|
|
39
|
-
return false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (isTty) {
|
|
43
|
-
return true
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Assume CI supports color
|
|
48
|
-
* @see {@link https://github.com/alexeyraspopov/picocolors/blob/0e7c4af2de299dd7bc5916f2bddd151fa2f66740/picocolors.js#L4}
|
|
49
|
-
* @see {@link https://github.com/tinylibs/tinyrainbow/blob/071034bf2eafa28d91ef0ba48a3837420d81a40a/src/index.ts#L91}
|
|
50
|
-
*/
|
|
51
|
-
if (TRUTHRY_ENV_VAR_VALUES.includes(p?.env?.CI)) {
|
|
52
|
-
return true
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (p?.env?.TERM && p.env.TERM === 'dumb') {
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Assume Windows supports color
|
|
61
|
-
* @see {@link https://github.com/alexeyraspopov/picocolors/blob/0e7c4af2de299dd7bc5916f2bddd151fa2f66740/picocolors.js#L4}
|
|
62
|
-
* @see {@link https://github.com/tinylibs/tinyrainbow/blob/071034bf2eafa28d91ef0ba48a3837420d81a40a/src/index.ts#L89}
|
|
63
|
-
*/
|
|
64
|
-
if (p?.platform === 'win32') {
|
|
65
|
-
return true
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return false
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* @deprecated replace this with Node.js builtin after minimum supported version is >=20.18.0
|
|
73
|
-
* @example util.styleText('red', 'test') !== 'text'
|
|
74
|
-
*/
|
|
75
|
-
export const SUPPORTS_COLOR = supportsAnsiColors()
|
|
76
|
-
|
|
77
|
-
const ANSI_RESET = '\u001B[0m'
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* @callback WrapAnsi
|
|
81
|
-
* @param {string} text
|
|
82
|
-
* @returns {string}
|
|
83
|
-
*/
|
|
84
|
-
/**
|
|
85
|
-
* @deprecated replace this with Node.js builtin after minimum supported version is >=20.18.0
|
|
86
|
-
* @example (format) => (text) => util.styleText(format, text)
|
|
87
|
-
*
|
|
88
|
-
* @param {string} code
|
|
89
|
-
* @param {boolean} [supported]
|
|
90
|
-
* @returns {WrapAnsi}
|
|
91
|
-
*
|
|
92
|
-
*/
|
|
93
|
-
export const wrapAnsiColor = (code, supported = SUPPORTS_COLOR) => {
|
|
94
|
-
if (supported) {
|
|
95
|
-
return (text) => code + text + ANSI_RESET
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return (text) => text
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export const red = wrapAnsiColor('\u001B[0;31m')
|
|
102
|
-
export const green = wrapAnsiColor('\u001B[0;32m')
|
|
103
|
-
export const yellow = wrapAnsiColor('\u001B[0;33m')
|
|
104
|
-
export const blue = wrapAnsiColor('\u001B[0;34m')
|
|
105
|
-
export const blackBright = wrapAnsiColor('\u001B[0;90m')
|
|
106
|
-
export const bold = wrapAnsiColor('\u001b[1m')
|
|
7
|
+
export const red = (text) => (SUPPORTS_COLOR ? util.styleText('red', text) : text)
|
|
8
|
+
export const yellow = (text) => (SUPPORTS_COLOR ? util.styleText('yellow', text) : text)
|
|
9
|
+
export const blue = (text) => (SUPPORTS_COLOR ? util.styleText('blue', text) : text)
|
|
10
|
+
export const dim = (text) => (SUPPORTS_COLOR ? util.styleText('dim', text) : text)
|
|
11
|
+
export const bold = (text) => (SUPPORTS_COLOR ? util.styleText('bold', text) : text)
|
package/lib/debug.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { formatWithOptions } from 'node:util'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { dim, SUPPORTS_COLOR } from './colors.js'
|
|
4
4
|
|
|
5
5
|
const format = (...args) => formatWithOptions({ colors: SUPPORTS_COLOR }, ...args)
|
|
6
6
|
|
|
7
7
|
let activeLogger
|
|
8
8
|
|
|
9
9
|
export const enableDebug = (logger = console) => {
|
|
10
|
-
|
|
11
|
-
activeLogger = logger
|
|
12
|
-
}
|
|
10
|
+
activeLogger = logger
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/** @param {string} name */
|
|
@@ -22,6 +20,6 @@ export const createDebug = (name) => {
|
|
|
22
20
|
const now = process.hrtime.bigint()
|
|
23
21
|
const ms = (now - previous) / 1_000_000n
|
|
24
22
|
previous = now
|
|
25
|
-
activeLogger.debug(
|
|
23
|
+
activeLogger.debug(dim(name + ': ') + format(...args) + dim(` +${ms}ms`))
|
|
26
24
|
}
|
|
27
25
|
}
|
package/lib/getRenderer.js
CHANGED
|
@@ -45,7 +45,7 @@ const getMainRendererOptions = ({ color, debug, quiet }, logger, env) => {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const getFallbackRenderer = ({ renderer }, { color
|
|
48
|
+
const getFallbackRenderer = ({ renderer }, { color }) => {
|
|
49
49
|
if (renderer === 'silent' || renderer === 'test' || !color) {
|
|
50
50
|
return renderer
|
|
51
51
|
}
|
package/lib/getSpawnedTask.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgsStringToArgv } from 'string-argv'
|
|
2
2
|
import { exec } from 'tinyexec'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { dim, red } from './colors.js'
|
|
5
5
|
import { createDebug } from './debug.js'
|
|
6
6
|
import { error, info } from './figures.js'
|
|
7
7
|
import { Signal } from './getAbortController.js'
|
|
@@ -51,7 +51,7 @@ const handleTaskOutput = (command, output, ctx, signal, errorResult) => {
|
|
|
51
51
|
*/
|
|
52
52
|
export const createTaskError = (command, result, ctx, signal = 'FAILED') => {
|
|
53
53
|
ctx.errors.add(TaskError)
|
|
54
|
-
return new Error(`${red(command)} ${
|
|
54
|
+
return new Error(`${red(command)} ${dim(`[${signal}]`)}`, { cause: result })
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
package/lib/gitWorkflow.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
2
|
import path from 'node:path'
|
|
4
3
|
|
|
5
4
|
import { createDebug } from './debug.js'
|
|
6
5
|
import { execGit } from './execGit.js'
|
|
7
6
|
import { readFile, unlink, writeFile } from './file.js'
|
|
8
7
|
import { getDiffCommand } from './getDiffCommand.js'
|
|
8
|
+
import { normalizePath } from './normalizePath.js'
|
|
9
9
|
import { parseGitZOutput } from './parseGitZOutput.js'
|
|
10
10
|
import {
|
|
11
11
|
ApplyEmptyCommitError,
|
|
@@ -79,26 +79,14 @@ const cleanGitStashOutput = (lines) => lines.map((line) => line.replace(/^"(.*)"
|
|
|
79
79
|
export class GitWorkflow {
|
|
80
80
|
/**
|
|
81
81
|
* @param {Object} opts
|
|
82
|
-
* @param {import('./getStagedFiles.js').StagedFile[][]} opts.matchedFileChunks
|
|
83
82
|
*/
|
|
84
|
-
constructor({
|
|
85
|
-
allowEmpty,
|
|
86
|
-
diff,
|
|
87
|
-
diffFilter,
|
|
88
|
-
failOnChanges,
|
|
89
|
-
gitConfigDir,
|
|
90
|
-
matchedFileChunks,
|
|
91
|
-
topLevelDir,
|
|
92
|
-
}) {
|
|
83
|
+
constructor({ allowEmpty, diff, diffFilter, failOnChanges, gitConfigDir, topLevelDir }) {
|
|
93
84
|
this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: topLevelDir })
|
|
94
85
|
this.allowEmpty = allowEmpty
|
|
95
|
-
this.deletedFiles = []
|
|
96
86
|
this.diff = diff
|
|
97
87
|
this.diffFilter = diffFilter
|
|
98
88
|
this.gitConfigDir = gitConfigDir
|
|
99
89
|
this.failOnChanges = !!failOnChanges
|
|
100
|
-
/** @type {import('./getStagedFiles.js').StagedFile[][]} */
|
|
101
|
-
this.matchedFileChunks = matchedFileChunks
|
|
102
90
|
this.topLevelDir = topLevelDir
|
|
103
91
|
|
|
104
92
|
/**
|
|
@@ -137,20 +125,6 @@ export class GitWorkflow {
|
|
|
137
125
|
return String(index)
|
|
138
126
|
}
|
|
139
127
|
|
|
140
|
-
/**
|
|
141
|
-
* Get a list of unstaged deleted files
|
|
142
|
-
*/
|
|
143
|
-
async getDeletedFiles() {
|
|
144
|
-
debugLog('Getting deleted files...')
|
|
145
|
-
const lsFiles = await this.execGit(['ls-files', '--deleted'])
|
|
146
|
-
const deletedFiles = lsFiles
|
|
147
|
-
.split('\n')
|
|
148
|
-
.filter(Boolean)
|
|
149
|
-
.map((file) => path.resolve(this.topLevelDir, file))
|
|
150
|
-
debugLog('Found deleted files:', deletedFiles)
|
|
151
|
-
return deletedFiles
|
|
152
|
-
}
|
|
153
|
-
|
|
154
128
|
/**
|
|
155
129
|
* Save meta information about ongoing git merge
|
|
156
130
|
*/
|
|
@@ -234,14 +208,9 @@ export class GitWorkflow {
|
|
|
234
208
|
if (ctx.shouldBackup) {
|
|
235
209
|
// When backup is enabled, the revert will clear ongoing merge status.
|
|
236
210
|
await this.backupMergeStatus()
|
|
237
|
-
|
|
238
|
-
// Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
|
|
239
|
-
// - in git versions =< 2.13.0 the `git stash --keep-index` option resurrects deleted files
|
|
240
|
-
// - git stash can't infer RD or MD states correctly, and will lose the deletion
|
|
241
|
-
this.deletedFiles = await this.getDeletedFiles()
|
|
242
211
|
}
|
|
243
212
|
|
|
244
|
-
if (ctx.shouldHideUnstaged) {
|
|
213
|
+
if (ctx.shouldHideUnstaged || ctx.shouldHideAll) {
|
|
245
214
|
this.unstagedFiles = await this.getUnstagedFiles({ onlyPartial: false })
|
|
246
215
|
ctx.hasFilesToHide = !!this.unstagedFiles
|
|
247
216
|
} else if (ctx.shouldHidePartiallyStaged) {
|
|
@@ -257,9 +226,11 @@ export class GitWorkflow {
|
|
|
257
226
|
}
|
|
258
227
|
|
|
259
228
|
if (ctx.shouldBackup) {
|
|
260
|
-
if (ctx.shouldHideUnstaged) {
|
|
229
|
+
if (ctx.shouldHideUnstaged || ctx.shouldHideAll) {
|
|
230
|
+
const args = ['stash', 'push', '--keep-index', '--message', STASH]
|
|
231
|
+
if (ctx.shouldHideAll) args.push('--include-untracked')
|
|
261
232
|
/** Save stash of all changes, clearing the working tree but keeping staged files as-is */
|
|
262
|
-
await this.execGit(
|
|
233
|
+
await this.execGit(args)
|
|
263
234
|
/** Print stash list with short hash and subject */
|
|
264
235
|
const stashes = await this.execGit(['stash', 'list', '--format="%h %s"', '-z'])
|
|
265
236
|
.then(parseGitZOutput)
|
|
@@ -285,7 +256,7 @@ export class GitWorkflow {
|
|
|
285
256
|
async hidePartiallyStagedChanges(ctx) {
|
|
286
257
|
try {
|
|
287
258
|
const files = processRenames(this.unstagedFiles, false)
|
|
288
|
-
await this.execGit(['
|
|
259
|
+
await this.execGit(['restore', '--worktree', '--', ...files])
|
|
289
260
|
} catch (error) {
|
|
290
261
|
/**
|
|
291
262
|
* `git checkout --force` doesn't throw errors, so it shouldn't be possible to get here.
|
|
@@ -308,11 +279,8 @@ export class GitWorkflow {
|
|
|
308
279
|
return task.newListr(listrTasks, { concurrent })
|
|
309
280
|
}
|
|
310
281
|
|
|
311
|
-
/**
|
|
312
|
-
|
|
313
|
-
* In case of a merge-conflict retry with 3-way merge.
|
|
314
|
-
*/
|
|
315
|
-
async applyModifications(ctx) {
|
|
282
|
+
/** Update Git index again for the originally staged files to stage task modifications. */
|
|
283
|
+
async updateIndex(ctx) {
|
|
316
284
|
if (ctx.shouldFailOnChanges) {
|
|
317
285
|
debugLog(
|
|
318
286
|
'Calculating SHA-256 hash of changes after tasks because "--fail-on-changes" was used...'
|
|
@@ -326,32 +294,18 @@ export class GitWorkflow {
|
|
|
326
294
|
}
|
|
327
295
|
}
|
|
328
296
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// Git add creates a lockfile in the repo causing concurrent operations to fail.
|
|
335
|
-
for (const files of this.matchedFileChunks) {
|
|
336
|
-
const accessCheckedFiles = await Promise.allSettled(
|
|
337
|
-
files.map(async (f) => {
|
|
338
|
-
if (f.status === 'D') {
|
|
339
|
-
await fs.access(f.filepath)
|
|
340
|
-
return f.filepath // File is no longer deleted and can be added
|
|
341
|
-
} else {
|
|
342
|
-
return f.filepath
|
|
343
|
-
}
|
|
344
|
-
})
|
|
345
|
-
)
|
|
297
|
+
// Unset GIT_INDEX_FILE so that the default index is always updated after running tasks
|
|
298
|
+
// Otherwise committing a pathspec (which uses a temporary non-default index) will leave
|
|
299
|
+
// changes in the worktree and default index
|
|
300
|
+
debugLog('Unset GIT_INDEX_FILE (was `%s`)', process.env.GIT_INDEX_FILE)
|
|
301
|
+
delete process.env.GIT_INDEX_FILE
|
|
346
302
|
|
|
347
|
-
|
|
348
|
-
r.status === 'fulfilled' ? [r.value] : []
|
|
349
|
-
)
|
|
303
|
+
debugLog('Updating Git index again after task modifications...')
|
|
350
304
|
|
|
351
|
-
|
|
352
|
-
|
|
305
|
+
// Update index for the files that were originally staged, ignore others
|
|
306
|
+
await this.execGit(['update-index', '--again'])
|
|
353
307
|
|
|
354
|
-
debugLog('Done
|
|
308
|
+
debugLog('Done updating Git index again after task modifications!')
|
|
355
309
|
|
|
356
310
|
const stagedFilesAfterAdd = await this.execGit([
|
|
357
311
|
...getDiffCommand(this.diff, this.diffFilter),
|
|
@@ -395,6 +349,42 @@ export class GitWorkflow {
|
|
|
395
349
|
}
|
|
396
350
|
}
|
|
397
351
|
|
|
352
|
+
async restoreUntrackedFiles(ctx) {
|
|
353
|
+
try {
|
|
354
|
+
debugLog('Restoring untracked files...')
|
|
355
|
+
const backupStash = await this.getBackupStash(ctx)
|
|
356
|
+
const untrackedFiles = await this.execGit([
|
|
357
|
+
'stash',
|
|
358
|
+
'show',
|
|
359
|
+
'--only-untracked',
|
|
360
|
+
'--name-only',
|
|
361
|
+
'-z',
|
|
362
|
+
backupStash,
|
|
363
|
+
]).then(parseGitZOutput)
|
|
364
|
+
|
|
365
|
+
if (untrackedFiles.length) {
|
|
366
|
+
debugLog('Found untracked files: %s', untrackedFiles)
|
|
367
|
+
await this.execGit([
|
|
368
|
+
'restore',
|
|
369
|
+
'--source',
|
|
370
|
+
`${ctx.backupHash}^3`,
|
|
371
|
+
'--',
|
|
372
|
+
...untrackedFiles.map(normalizePath),
|
|
373
|
+
])
|
|
374
|
+
} else {
|
|
375
|
+
debugLog('No untracked files to restore!')
|
|
376
|
+
}
|
|
377
|
+
} catch (restoreUntrackedFilesError) {
|
|
378
|
+
debugLog('Error while restoring untracked files:')
|
|
379
|
+
debugLog(restoreUntrackedFilesError)
|
|
380
|
+
handleError(
|
|
381
|
+
new Error('Untracked files could not be restored!'),
|
|
382
|
+
ctx,
|
|
383
|
+
RestoreUnstagedChangesError
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
398
388
|
/**
|
|
399
389
|
* Restore original HEAD state in case of errors
|
|
400
390
|
*/
|
|
@@ -407,9 +397,6 @@ export class GitWorkflow {
|
|
|
407
397
|
// Restore meta information about ongoing git merge
|
|
408
398
|
await this.restoreMergeStatus(ctx)
|
|
409
399
|
|
|
410
|
-
// If stashing resurrected deleted files, clean them out
|
|
411
|
-
await Promise.all(this.deletedFiles.map((file) => unlink(file)))
|
|
412
|
-
|
|
413
400
|
// Clean out patch
|
|
414
401
|
await unlink(this.getHiddenFilepath(PATCH_UNSTAGED))
|
|
415
402
|
|
package/lib/index.d.ts
CHANGED
|
@@ -81,6 +81,11 @@ export type Options = {
|
|
|
81
81
|
* @default false
|
|
82
82
|
*/
|
|
83
83
|
hideUnstaged?: boolean
|
|
84
|
+
/**
|
|
85
|
+
* Whether to hide all unstaged changes and untracked files before running tasks
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
hideAll?: boolean
|
|
84
89
|
/**
|
|
85
90
|
* Disable lint-staged’s own console output
|
|
86
91
|
* @default false
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { assertGitVersion, MIN_GIT_VERSION } from './assertGitVersion.js'
|
|
1
2
|
import { SUPPORTS_COLOR } from './colors.js'
|
|
2
3
|
import { createDebug, enableDebug } from './debug.js'
|
|
3
4
|
import { execGit } from './execGit.js'
|
|
4
5
|
import {
|
|
5
6
|
GIT_ERROR,
|
|
7
|
+
minGitVersionRequired,
|
|
6
8
|
NO_CONFIGURATION,
|
|
7
9
|
PREVENTED_EMPTY_COMMIT,
|
|
8
10
|
PREVENTED_TASK_MODIFICATIONS,
|
|
@@ -86,8 +88,9 @@ const lintStaged = async (
|
|
|
86
88
|
diff,
|
|
87
89
|
diffFilter,
|
|
88
90
|
failOnChanges = false,
|
|
91
|
+
hideAll = false,
|
|
89
92
|
hideUnstaged = false,
|
|
90
|
-
hidePartiallyStaged = !hideUnstaged,
|
|
93
|
+
hidePartiallyStaged = !(hideAll || hideUnstaged),
|
|
91
94
|
maxArgLength = getMaxArgLength() / 2,
|
|
92
95
|
quiet = false,
|
|
93
96
|
relative = false,
|
|
@@ -114,6 +117,10 @@ const lintStaged = async (
|
|
|
114
117
|
const gitVersion = await execGit(['version', '--build-options'], { cwd })
|
|
115
118
|
debugLog('%s', gitVersion)
|
|
116
119
|
|
|
120
|
+
if (!assertGitVersion(gitVersion)) {
|
|
121
|
+
throw new Error(minGitVersionRequired(MIN_GIT_VERSION, gitVersion), { cause: gitVersion })
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
const options = {
|
|
118
125
|
allowEmpty,
|
|
119
126
|
color,
|
|
@@ -126,6 +133,7 @@ const lintStaged = async (
|
|
|
126
133
|
diff,
|
|
127
134
|
diffFilter,
|
|
128
135
|
failOnChanges,
|
|
136
|
+
hideAll,
|
|
129
137
|
hidePartiallyStaged,
|
|
130
138
|
hideUnstaged,
|
|
131
139
|
maxArgLength,
|
package/lib/messages.js
CHANGED
|
@@ -67,12 +67,13 @@ export const PREVENTED_EMPTY_COMMIT = `
|
|
|
67
67
|
`
|
|
68
68
|
|
|
69
69
|
export const restoreStashExample = (
|
|
70
|
-
hash = '
|
|
70
|
+
hash = '<git-hash>'
|
|
71
71
|
) => `Any lost modifications can be restored from a git stash:
|
|
72
72
|
|
|
73
73
|
> git stash list --format="%h %s"
|
|
74
74
|
${hash} On main: lint-staged automatic backup
|
|
75
|
-
> git apply --index ${hash}
|
|
75
|
+
> git apply --index ${hash}
|
|
76
|
+
`
|
|
76
77
|
|
|
77
78
|
export const CONFIG_STDIN_ERROR = red(`${error} Failed to read config from stdin.`)
|
|
78
79
|
|
|
@@ -89,3 +90,8 @@ ${error}
|
|
|
89
90
|
See https://github.com/okonet/lint-staged#configuration.`
|
|
90
91
|
|
|
91
92
|
export const UNSTAGED_CHANGES_BACKUP_STASH_LOCATION = `Unstaged changes have been kept back in a patch file:`
|
|
93
|
+
|
|
94
|
+
export const minGitVersionRequired = (expected) =>
|
|
95
|
+
red(`${error} lint-staged requires at least Git version ${bold(expected)}.
|
|
96
|
+
|
|
97
|
+
Please update Git: https://git-scm.com/downloads`)
|
package/lib/runAll.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path'
|
|
|
5
5
|
import { Listr } from 'listr2'
|
|
6
6
|
|
|
7
7
|
import { chunkFiles } from './chunkFiles.js'
|
|
8
|
-
import {
|
|
8
|
+
import { dim } from './colors.js'
|
|
9
9
|
import { createDebug } from './debug.js'
|
|
10
10
|
import { execGit } from './execGit.js'
|
|
11
11
|
import { generateTasks } from './generateTasks.js'
|
|
@@ -30,7 +30,6 @@ import { normalizePath } from './normalizePath.js'
|
|
|
30
30
|
import { resolveGitRepo } from './resolveGitRepo.js'
|
|
31
31
|
import { searchConfigs } from './searchConfigs.js'
|
|
32
32
|
import {
|
|
33
|
-
applyModificationsSkipped,
|
|
34
33
|
cleanupEnabled,
|
|
35
34
|
cleanupSkipped,
|
|
36
35
|
getInitialState,
|
|
@@ -39,6 +38,8 @@ import {
|
|
|
39
38
|
restoreUnstagedChangesSkipped,
|
|
40
39
|
shouldHidePartiallyStagedFiles,
|
|
41
40
|
shouldRestoreUnstagedChanges,
|
|
41
|
+
shouldRestoreUntrackedFiles,
|
|
42
|
+
updateIndexSkipped,
|
|
42
43
|
} from './state.js'
|
|
43
44
|
import { ConfigNotFoundError, GetStagedFilesError, GitError, GitRepoError } from './symbols.js'
|
|
44
45
|
|
|
@@ -90,8 +91,9 @@ export const runAll = async (
|
|
|
90
91
|
diff,
|
|
91
92
|
diffFilter,
|
|
92
93
|
failOnChanges = false,
|
|
94
|
+
hideAll = false,
|
|
93
95
|
hideUnstaged = false,
|
|
94
|
-
hidePartiallyStaged = !hideUnstaged,
|
|
96
|
+
hidePartiallyStaged = !(hideAll || hideUnstaged),
|
|
95
97
|
maxArgLength,
|
|
96
98
|
quiet = false,
|
|
97
99
|
relative = false,
|
|
@@ -112,8 +114,9 @@ export const runAll = async (
|
|
|
112
114
|
|
|
113
115
|
const ctx = getInitialState({
|
|
114
116
|
failOnChanges,
|
|
115
|
-
|
|
117
|
+
hideAll,
|
|
116
118
|
hideUnstaged,
|
|
119
|
+
hidePartiallyStaged,
|
|
117
120
|
quiet,
|
|
118
121
|
revert,
|
|
119
122
|
})
|
|
@@ -138,7 +141,7 @@ export const runAll = async (
|
|
|
138
141
|
logger.warn(skippingBackup(hasInitialCommit, diff))
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
if (!ctx.shouldHidePartiallyStaged && !ctx.shouldHideUnstaged && !quiet) {
|
|
144
|
+
if (!ctx.shouldHidePartiallyStaged && !ctx.shouldHideUnstaged && !ctx.shouldHideAll && !quiet) {
|
|
142
145
|
logger.warn(SKIPPING_HIDE_PARTIALLY_CHANGED)
|
|
143
146
|
}
|
|
144
147
|
|
|
@@ -204,7 +207,7 @@ export const runAll = async (
|
|
|
204
207
|
// Use actual cwd if it's specified, or there's only a single config file.
|
|
205
208
|
// Otherwise use the directory of the config file for each config group,
|
|
206
209
|
// to make sure tasks are separated from each other.
|
|
207
|
-
const groupCwd =
|
|
210
|
+
const groupCwd = hasExplicitCwd || !hasMultipleConfigs ? cwd : path.dirname(configPath)
|
|
208
211
|
|
|
209
212
|
const chunkCount = stagedFileChunks.length
|
|
210
213
|
if (chunkCount > 1) {
|
|
@@ -248,7 +251,7 @@ export const runAll = async (
|
|
|
248
251
|
const fileCount = task.fileList.length
|
|
249
252
|
|
|
250
253
|
return {
|
|
251
|
-
title: `${task.pattern}${
|
|
254
|
+
title: `${task.pattern}${dim(
|
|
252
255
|
` — ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`
|
|
253
256
|
)}`,
|
|
254
257
|
task: async (ctx, task) =>
|
|
@@ -260,7 +263,7 @@ export const runAll = async (
|
|
|
260
263
|
skip: () => {
|
|
261
264
|
// Skip task when no files matched
|
|
262
265
|
if (fileCount === 0) {
|
|
263
|
-
return `${task.pattern}${
|
|
266
|
+
return `${task.pattern}${dim(' — no files')}`
|
|
264
267
|
}
|
|
265
268
|
return false
|
|
266
269
|
},
|
|
@@ -271,8 +274,8 @@ export const runAll = async (
|
|
|
271
274
|
|
|
272
275
|
listrTasks.push({
|
|
273
276
|
title:
|
|
274
|
-
`${configName}${
|
|
275
|
-
(chunkCount > 1 ?
|
|
277
|
+
`${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
|
|
278
|
+
(chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
|
|
276
279
|
task: (ctx, task) =>
|
|
277
280
|
task.newListr(chunkListrTasks, { concurrent, exitOnError: !continueOnError }),
|
|
278
281
|
skip: () => {
|
|
@@ -280,7 +283,7 @@ export const runAll = async (
|
|
|
280
283
|
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
|
281
284
|
// Skip chunk when no every task is skipped (due to no matches)
|
|
282
285
|
if (chunkListrTasks.every((task) => task.skip())) {
|
|
283
|
-
return `${configName}${
|
|
286
|
+
return `${configName}${dim(' — no tasks to run')}`
|
|
284
287
|
}
|
|
285
288
|
return false
|
|
286
289
|
},
|
|
@@ -299,23 +302,12 @@ export const runAll = async (
|
|
|
299
302
|
return ctx
|
|
300
303
|
}
|
|
301
304
|
|
|
302
|
-
// Chunk matched files for better Windows compatibility
|
|
303
|
-
/** @type {import('./getStagedFiles.js').StagedFile[][]} */
|
|
304
|
-
const matchedFileChunks = chunkFiles({
|
|
305
|
-
// matched files are relative to `cwd`, not `topLevelDir`, when `relative` is used
|
|
306
|
-
baseDir: cwd,
|
|
307
|
-
files: Array.from(matchedFiles),
|
|
308
|
-
maxArgLength,
|
|
309
|
-
relative: false,
|
|
310
|
-
})
|
|
311
|
-
|
|
312
305
|
const git = new GitWorkflow({
|
|
313
306
|
allowEmpty,
|
|
314
307
|
diff,
|
|
315
308
|
diffFilter,
|
|
316
309
|
failOnChanges,
|
|
317
310
|
gitConfigDir,
|
|
318
|
-
matchedFileChunks,
|
|
319
311
|
topLevelDir,
|
|
320
312
|
})
|
|
321
313
|
|
|
@@ -336,9 +328,9 @@ export const runAll = async (
|
|
|
336
328
|
skip: () => listrTasks.every((task) => task.skip()),
|
|
337
329
|
},
|
|
338
330
|
{
|
|
339
|
-
title: '
|
|
340
|
-
task: (ctx) => git.
|
|
341
|
-
skip:
|
|
331
|
+
title: 'Updating Git index again...',
|
|
332
|
+
task: (ctx) => git.updateIndex(ctx),
|
|
333
|
+
skip: updateIndexSkipped,
|
|
342
334
|
},
|
|
343
335
|
{
|
|
344
336
|
title: 'Restoring unstaged changes...',
|
|
@@ -346,6 +338,12 @@ export const runAll = async (
|
|
|
346
338
|
enabled: shouldRestoreUnstagedChanges,
|
|
347
339
|
skip: restoreUnstagedChangesSkipped,
|
|
348
340
|
},
|
|
341
|
+
{
|
|
342
|
+
title: 'Restoring untracked files...',
|
|
343
|
+
task: (ctx) => git.restoreUntrackedFiles(ctx),
|
|
344
|
+
enabled: shouldRestoreUntrackedFiles,
|
|
345
|
+
skip: restoreUnstagedChangesSkipped,
|
|
346
|
+
},
|
|
349
347
|
{
|
|
350
348
|
title: 'Reverting to original state because of errors...',
|
|
351
349
|
task: (ctx) => git.restoreOriginalState(ctx),
|
package/lib/state.js
CHANGED
|
@@ -9,32 +9,48 @@ import {
|
|
|
9
9
|
|
|
10
10
|
export const getInitialState = ({
|
|
11
11
|
failOnChanges = false,
|
|
12
|
+
hideAll = false,
|
|
12
13
|
hideUnstaged = false,
|
|
13
|
-
hidePartiallyStaged = !hideUnstaged,
|
|
14
|
+
hidePartiallyStaged = !(hideAll || hideUnstaged),
|
|
14
15
|
quiet = false,
|
|
15
16
|
revert = true,
|
|
16
|
-
} = {}) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
} = {}) => {
|
|
18
|
+
const initialState = {
|
|
19
|
+
backupHash: null,
|
|
20
|
+
errors: new Set([]),
|
|
21
|
+
shouldFailOnChanges: failOnChanges,
|
|
22
|
+
hasFilesToHide: null,
|
|
23
|
+
output: [],
|
|
24
|
+
quiet,
|
|
25
|
+
shouldBackup: null,
|
|
26
|
+
shouldHideAll: hideAll,
|
|
27
|
+
shouldHideUnstaged: hideUnstaged,
|
|
28
|
+
shouldHidePartiallyStaged: hidePartiallyStaged,
|
|
29
|
+
shouldRevert: revert,
|
|
30
|
+
unstagedDiffSha256: null,
|
|
31
|
+
unstagedPatch: null,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (initialState.shouldHideAll) {
|
|
35
|
+
initialState.shouldHideUnstaged = false // becomes redundant
|
|
36
|
+
initialState.shouldHidePartiallyStaged = false // becomes redundant
|
|
37
|
+
} else if (initialState.shouldHideUnstaged) {
|
|
38
|
+
initialState.shouldHidePartiallyStaged = false // becomes redundant
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return initialState
|
|
42
|
+
}
|
|
30
43
|
|
|
31
44
|
export const shouldHidePartiallyStagedFiles = (ctx) =>
|
|
32
45
|
ctx.shouldHidePartiallyStaged && ctx.hasFilesToHide
|
|
33
46
|
|
|
34
47
|
export const shouldRestoreUnstagedChanges = (ctx) =>
|
|
35
|
-
(ctx.shouldHideUnstaged || ctx.shouldHidePartiallyStaged) &&
|
|
48
|
+
(ctx.shouldHideAll || ctx.shouldHideUnstaged || ctx.shouldHidePartiallyStaged) &&
|
|
49
|
+
ctx.hasFilesToHide
|
|
50
|
+
|
|
51
|
+
export const shouldRestoreUntrackedFiles = (ctx) => !!ctx.shouldHideAll
|
|
36
52
|
|
|
37
|
-
export const
|
|
53
|
+
export const updateIndexSkipped = (ctx) => {
|
|
38
54
|
// Always apply back unstaged modifications when skipping revert or backup
|
|
39
55
|
if (!ctx.shouldRevert || !ctx.shouldBackup) return false
|
|
40
56
|
|