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/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
- import nodeTty from 'node:tty'
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
- * @returns `true` if ANSI colors are supported
18
- *
19
- * @param {NodeJS.Process} [p]
20
- * @param {boolean} [isTty]
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 { blackBright, SUPPORTS_COLOR } from './colors.js'
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
- if (!activeLogger) {
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(blackBright(name + ': ') + format(...args) + blackBright(` +${ms}ms`))
23
+ activeLogger.debug(dim(name + ': ') + format(...args) + dim(` +${ms}ms`))
26
24
  }
27
25
  }
@@ -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 filteredFiles = relativeFiles.filter((file) => {
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 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
- )
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)
@@ -45,7 +45,7 @@ const getMainRendererOptions = ({ color, debug, quiet }, logger, env) => {
45
45
  }
46
46
  }
47
47
 
48
- const getFallbackRenderer = ({ renderer }, { color = false }) => {
48
+ const getFallbackRenderer = ({ renderer }, { color }) => {
49
49
  if (renderer === 'silent' || renderer === 'test' || !color) {
50
50
  return renderer
51
51
  }
@@ -1,7 +1,7 @@
1
1
  import { parseArgsStringToArgv } from 'string-argv'
2
2
  import { exec } from 'tinyexec'
3
3
 
4
- import { blackBright, red } from './colors.js'
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)} ${blackBright(`[${signal}]`)}`, { cause: result })
54
+ return new Error(`${red(command)} ${dim(`[${signal}]`)}`, { cause: result })
55
55
  }
56
56
 
57
57
  /**
@@ -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(['stash', 'push', '--keep-index', '--message', STASH])
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(['checkout', '--force', '--', ...files])
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
- * Applies back task modifications, and unstaged changes hidden in the stash.
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
- debugLog('Adding task modifications to index...')
330
-
331
- // `matchedFileChunks` includes staged files that lint-staged originally detected and matched against a task.
332
- // Add only these files so any 3rd-party edits to other files won't be included in the commit.
333
- // These additions per chunk are run "serially" to prevent race conditions.
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
- const addableFiles = accessCheckedFiles.flatMap((r) =>
348
- r.status === 'fulfilled' ? [r.value] : []
349
- )
303
+ debugLog('Updating Git index again after task modifications...')
350
304
 
351
- await this.execGit(['add', '--', ...addableFiles])
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 adding task modifications to index!')
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