lint-staged 16.3.4 → 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 +48 -44
- 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/generateTasks.js +6 -28
- 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/matchFiles.js +22 -0
- 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/cli.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { parseArgs } from 'node:util'
|
|
5
|
+
|
|
6
|
+
import { restoreStashExample } from './messages.js'
|
|
7
|
+
|
|
8
|
+
const CLI_OPTIONS = [
|
|
9
|
+
{
|
|
10
|
+
short: 'h',
|
|
11
|
+
flag: 'help',
|
|
12
|
+
type: 'boolean',
|
|
13
|
+
description: 'display this help message',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
short: 'V',
|
|
17
|
+
flag: 'version',
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
description: 'display the current version number',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
flag: 'allow-empty',
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
description: 'allow empty commits when tasks revert all staged changes (default: false)',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
short: 'p',
|
|
28
|
+
flag: 'concurrent',
|
|
29
|
+
positional: '<number|boolean>',
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'the number of tasks to run concurrently, or false for serial (default: true)',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
short: 'c',
|
|
35
|
+
flag: 'config',
|
|
36
|
+
positional: '[path]',
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'path to configuration file, or - to read from stdin',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
flag: 'continue-on-error',
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
description: 'run all tasks to completion even if one fails (default: false)',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
flag: 'cwd',
|
|
47
|
+
positional: '[path]',
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: 'run all tasks in specific directory, instead of the current',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
short: 'd',
|
|
53
|
+
flag: 'debug',
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
description: 'print additional debug information (default: false)',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
flag: 'diff',
|
|
59
|
+
positional: '[string]',
|
|
60
|
+
type: 'string',
|
|
61
|
+
description:
|
|
62
|
+
'override the default "--staged" flag of "git diff" to get list of files. Implies "--no-stash".',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
flag: 'diff-filter',
|
|
66
|
+
positional: '[string]',
|
|
67
|
+
type: 'string',
|
|
68
|
+
description:
|
|
69
|
+
'override the default "--diff-filter=ACMR" flag of "git diff" to get list of files',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
flag: 'fail-on-changes',
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
description: 'fail with exit code 1 when tasks modify tracked files (default: false)',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
negative: true,
|
|
78
|
+
flag: 'hide-partially-staged',
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
description: 'hide unstaged changes from partially staged files (default: true)',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
flag: 'hide-unstaged',
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
description: 'hide all unstaged changes, instead of just partially staged (default: false)',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
flag: 'hide-all',
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
description: 'hide all unstaged changes and untracked files (default: false)',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
flag: 'max-arg-length',
|
|
94
|
+
type: 'string', // Parsed with `parseInt()` below
|
|
95
|
+
positional: '[number]',
|
|
96
|
+
description: 'maximum length of the command-line argument string (default: 0)',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
short: 'q',
|
|
100
|
+
flag: 'quiet',
|
|
101
|
+
type: 'boolean',
|
|
102
|
+
description: "disable lint-staged's own console output (default: false)",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
short: 'r',
|
|
106
|
+
flag: 'relative',
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
description: 'pass relative filepaths to tasks (default: false)',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
negative: true,
|
|
112
|
+
flag: 'revert',
|
|
113
|
+
type: 'boolean',
|
|
114
|
+
description: 'revert to original state in case of errors (default: true)',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
negative: true,
|
|
118
|
+
flag: 'stash',
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
description: 'enable the backup stash (default: true)',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
short: 'v',
|
|
124
|
+
flag: 'verbose',
|
|
125
|
+
type: 'boolean',
|
|
126
|
+
description:
|
|
127
|
+
'show task output even when tasks succeed; by default only failed output is shown (default: false)',
|
|
128
|
+
},
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
/** @param {string[]} argv */
|
|
132
|
+
export const parseCliOptions = (argv) => {
|
|
133
|
+
const options = CLI_OPTIONS.reduce((acc, current) => {
|
|
134
|
+
acc[current.flag] = { type: current.type }
|
|
135
|
+
if (current.short) acc[current.flag].short = current.short
|
|
136
|
+
return acc
|
|
137
|
+
}, {})
|
|
138
|
+
|
|
139
|
+
const { values } = parseArgs({
|
|
140
|
+
args: argv,
|
|
141
|
+
allowNegative: true,
|
|
142
|
+
allowPositionals: true,
|
|
143
|
+
options,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
if (values.diff !== undefined && values.stash === undefined) {
|
|
147
|
+
/** Disable stashing by default when diffing specific value */
|
|
148
|
+
values.stash = false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (values['fail-on-changes'] && values.revert === undefined) {
|
|
152
|
+
/** When using --fail-on-changes, default to not reverting on errors */
|
|
153
|
+
values.revert = false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (values.stash === false && values.revert === undefined) {
|
|
157
|
+
/** Can't revert when using --no-stash */
|
|
158
|
+
values.revert = false
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (values['hide-unstaged'] === true) {
|
|
162
|
+
values['hide-partially-staged'] = false // becomes redundant
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (values['hide-all'] === true) {
|
|
166
|
+
values['hide-partially-staged'] = false // becomes redundant
|
|
167
|
+
values['hide-unstaged'] = false // becomes redundant
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
allowEmpty: values['allow-empty'] ?? false,
|
|
172
|
+
concurrent: values.concurrent === undefined ? true : JSON.parse(values.concurrent),
|
|
173
|
+
configPath: values.config,
|
|
174
|
+
continueOnError: !!values['continue-on-error'],
|
|
175
|
+
cwd: values.cwd,
|
|
176
|
+
debug: !!values.debug,
|
|
177
|
+
diff: values.diff,
|
|
178
|
+
diffFilter: values['diff-filter'],
|
|
179
|
+
failOnChanges: !!values['fail-on-changes'],
|
|
180
|
+
help: !!values.help,
|
|
181
|
+
hidePartiallyStaged: values['hide-partially-staged'] ?? true,
|
|
182
|
+
hideUnstaged: !!values['hide-unstaged'],
|
|
183
|
+
hideAll: !!values['hide-all'],
|
|
184
|
+
maxArgLength: parseInt(values['max-arg-length'], 10),
|
|
185
|
+
quiet: !!values.quiet,
|
|
186
|
+
relative: !!values.relative,
|
|
187
|
+
revert: values.revert ?? true,
|
|
188
|
+
stash: values.stash ?? true,
|
|
189
|
+
verbose: !!values.verbose,
|
|
190
|
+
version: !!values.version,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const getVersionNumber = async () => {
|
|
195
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
196
|
+
const packageJsonFile = await readFile(path.join(dirname, '../package.json'), 'utf-8')
|
|
197
|
+
/** @type {import('../package.json')} */
|
|
198
|
+
const packageJson = JSON.parse(packageJsonFile)
|
|
199
|
+
|
|
200
|
+
return packageJson.version
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const helpOptions = CLI_OPTIONS.map((option) => {
|
|
204
|
+
if (option.negative) {
|
|
205
|
+
/** @example `--no-stash` */
|
|
206
|
+
return [`--no-${option.flag}`, option.description]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @example `-V, --version
|
|
211
|
+
* or
|
|
212
|
+
* @example `--allow-empty`
|
|
213
|
+
*/
|
|
214
|
+
let arg = option.short ? `-${option.short}, --${option.flag}` : `--${option.flag}`
|
|
215
|
+
|
|
216
|
+
/** @example `--cwd [path]` */
|
|
217
|
+
if (option.positional) arg += ` ${option.positional}`
|
|
218
|
+
|
|
219
|
+
return [arg, option.description]
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const createWrap = (width) => {
|
|
223
|
+
const regExp = new RegExp(`.{1,${width}}(\\s|$)`, 'g')
|
|
224
|
+
return (text) => text.match(regExp)?.map((s) => s.trimEnd())
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const printHelpText = async (width = process.stdout.columns ?? 80) => {
|
|
228
|
+
const output = ['Usage: lint-staged [options]', '']
|
|
229
|
+
|
|
230
|
+
const col1Width = Math.max(...helpOptions.map(([arg]) => arg.length)) + 2
|
|
231
|
+
const wrap = createWrap(width - col1Width)
|
|
232
|
+
|
|
233
|
+
for (const [arg, description] of helpOptions) {
|
|
234
|
+
const lines = wrap(description)
|
|
235
|
+
const pad = ' '.repeat(col1Width)
|
|
236
|
+
output.push(arg.padEnd(col1Width) + lines[0], ...lines.slice(1).map((line) => pad + line))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
output.push('', restoreStashExample())
|
|
240
|
+
|
|
241
|
+
return output.join('\n')
|
|
242
|
+
}
|
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/generateTasks.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
|
-
import micromatch from 'micromatch'
|
|
4
|
-
|
|
5
3
|
import { createDebug } from './debug.js'
|
|
4
|
+
import { matchFiles } from './matchFiles.js'
|
|
6
5
|
import { normalizePath } from './normalizePath.js'
|
|
7
6
|
|
|
8
7
|
const debugLog = createDebug('lint-staged:generateTasks')
|
|
@@ -30,36 +29,15 @@ export const generateTasks = ({ config, cwd = process.cwd(), files, relative = f
|
|
|
30
29
|
|
|
31
30
|
// Only worry about children of the CWD unless the pattern explicitly
|
|
32
31
|
// specifies that it concerns a parent directory.
|
|
33
|
-
const
|
|
32
|
+
const includedFiles = relativeFiles.filter((file) => {
|
|
34
33
|
if (isParentDirPattern) return true
|
|
35
34
|
return !file.filepath.startsWith('..') && !path.isAbsolute(file.filepath)
|
|
36
35
|
})
|
|
37
36
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
)
|
|
52
|
-
|
|
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
|
-
)
|
|
37
|
+
const fileList = matchFiles(includedFiles, pattern, cwd).map((file) => ({
|
|
38
|
+
filepath: normalizePath(relative ? file.filepath : path.resolve(cwd, file.filepath)),
|
|
39
|
+
status: file.status,
|
|
40
|
+
}))
|
|
63
41
|
|
|
64
42
|
const task = { pattern, commands, fileList }
|
|
65
43
|
debugLog('Generated task: \n%O', task)
|
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
|