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/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
  }
@@ -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
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 = 'h0a0s0h0'
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 { blackBright } from './colors.js'
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
- hidePartiallyStaged,
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 = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd
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}${blackBright(
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}${blackBright(' — no files')}`
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}${blackBright(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
275
- (chunkCount > 1 ? blackBright(` (chunk ${index + 1}/${chunkCount})...`) : ''),
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}${blackBright(' — no tasks to run')}`
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: 'Applying modifications from tasks...',
340
- task: (ctx) => git.applyModifications(ctx),
341
- skip: applyModificationsSkipped,
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
- backupHash: null,
18
- errors: new Set([]),
19
- shouldFailOnChanges: failOnChanges,
20
- hasFilesToHide: null,
21
- output: [],
22
- quiet,
23
- shouldBackup: null,
24
- shouldHidePartiallyStaged: hidePartiallyStaged,
25
- shouldHideUnstaged: hideUnstaged,
26
- shouldRevert: revert,
27
- unstagedDiffSha256: null,
28
- unstagedPatch: null,
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) && ctx.hasFilesToHide
48
+ (ctx.shouldHideAll || ctx.shouldHideUnstaged || ctx.shouldHidePartiallyStaged) &&
49
+ ctx.hasFilesToHide
50
+
51
+ export const shouldRestoreUntrackedFiles = (ctx) => !!ctx.shouldHideAll
36
52
 
37
- export const applyModificationsSkipped = (ctx) => {
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