lint-staged 17.0.5 → 17.0.7

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/README.md CHANGED
@@ -772,6 +772,23 @@ module.exports = {
772
772
 
773
773
  ## Frequently Asked Questions
774
774
 
775
+ ### How does `lint-staged`'s stashing work?
776
+
777
+ <details>
778
+ <summary>Click to expand</summary>
779
+
780
+ When running `lint-staged` with the default configuration, the following happens:
781
+
782
+ 1. The entire original state is backed up in a git stash using `git stash create` and `git stash store`. This leaves all files in the worktree by default — the regular `git stash` command would also remove them. This most probably ignores any untracked files, which is the default behavior of `git stash`.
783
+ 1. If some file is "_partially staged_", meaning there's both staged and unstaged changes in the same file, lint-staged will additionally save all of these in a patch file, basically like `git diff --patch >> .git/lint-staged_unstaged.patch`. After this, the unstaged changes to these files are removed from the worktree so that tasks only see the staged changes
784
+ 1. After running tasks, any new modifications to the originally staged files are added to the index
785
+ 1. After this, any originally unstaged changes to "_partially staged_" files are restored by applying the patch file from step 2.
786
+ 1. In case of any errors, the state is reset with `git reset` and the original state restored from the git stash created in step 1.
787
+
788
+ If the process is interrupted at any point, it should be possible to restore changes from the git stash created in step 1. because it's only dropped in the last step, after successfully completing all previous steps in the process.
789
+
790
+ </details>
791
+
775
792
  ### The output of commit hook looks weird (no colors, duplicate lines, verbose output on Windows, …)
776
793
 
777
794
  <details>
package/lib/execGit.js CHANGED
@@ -23,7 +23,6 @@ export const execGit = async (cmd, options) => {
23
23
  nodeOptions: {
24
24
  env: options?.env,
25
25
  cwd: options?.cwd,
26
- stdio: ['ignore'],
27
26
  },
28
27
  })
29
28
 
@@ -91,7 +91,6 @@ export const getSpawnedTask = ({
91
91
  // e.g `npm` should run tasks in the actual CWD
92
92
  cwd: /^git(\.exe)?/i.test(cmd) ? topLevelDir : cwd,
93
93
  env: color ? { FORCE_COLOR: 'true' } : { NO_COLOR: 'true' },
94
- stdio: ['ignore'],
95
94
  },
96
95
  }
97
96
 
@@ -1,4 +1,5 @@
1
1
  import crypto from 'node:crypto'
2
+ import fs from 'node:fs/promises'
2
3
  import path from 'node:path'
3
4
 
4
5
  import { createDebug } from './debug.js'
@@ -80,13 +81,23 @@ export class GitWorkflow {
80
81
  /**
81
82
  * @param {Object} opts
82
83
  */
83
- constructor({ allowEmpty, diff, diffFilter, failOnChanges, gitConfigDir, topLevelDir }) {
84
+ constructor({
85
+ allowEmpty,
86
+ diff,
87
+ diffFilter,
88
+ failOnChanges,
89
+ gitConfigDir,
90
+ matchedFileChunks,
91
+ topLevelDir,
92
+ }) {
84
93
  this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: topLevelDir })
85
94
  this.allowEmpty = allowEmpty
86
95
  this.diff = diff
87
96
  this.diffFilter = diffFilter
88
97
  this.gitConfigDir = gitConfigDir
89
98
  this.failOnChanges = !!failOnChanges
99
+ /** @type {import('./getStagedFiles.js').StagedFile[][]} */
100
+ this.matchedFileChunks = matchedFileChunks
90
101
  this.topLevelDir = topLevelDir
91
102
 
92
103
  /**
@@ -299,30 +310,46 @@ export class GitWorkflow {
299
310
  ? normalizePath(process.env.GIT_INDEX_FILE)
300
311
  : process.env.GIT_INDEX_FILE
301
312
 
302
- debugLog('Updating active Git index again: %s', activeIndexFile)
303
- await this.execGit(['update-index', '--again'])
304
- debugLog('Done updating Git index again: %s', activeIndexFile)
313
+ /** Needs to be run serially because of locking Git operation */
314
+ for (const files of this.matchedFileChunks) {
315
+ const accessCheckedFiles = await Promise.allSettled(
316
+ files.map(async (f) => {
317
+ if (f.status === 'D') {
318
+ await fs.access(f.filepath)
319
+ return f.filepath // File is no longer deleted and can be added
320
+ } else {
321
+ return f.filepath
322
+ }
323
+ })
324
+ )
305
325
 
306
- if (activeIndexFile?.endsWith('.lock')) {
307
- const defaultIndexLock = normalizePath(
308
- await this.execGit(['rev-parse', '--path-format=absolute', '--git-path', 'index.lock'])
326
+ const addableFiles = accessCheckedFiles.flatMap((r) =>
327
+ r.status === 'fulfilled' ? [r.value] : []
309
328
  )
310
329
 
311
- /**
312
- * If the active index file is a non-default lockfile, we are committing with a pathspec
313
- * without having explicitly run `git add`. In this case we need to also update the
314
- * default index, otherwise there will be leftover diff after committing
315
- */
316
- if (activeIndexFile !== defaultIndexLock) {
317
- debugLog('Updating default Git index lock again: %s', defaultIndexLock)
330
+ debugLog('Updating active Git index: %s', activeIndexFile)
331
+ await this.execGit(['add', '--', ...addableFiles])
332
+ debugLog('Done updating Git index: %s', activeIndexFile)
318
333
 
319
- await this.execGit(['update-index', '--again'], {
320
- env: {
321
- GIT_INDEX_FILE: defaultIndexLock,
322
- },
323
- })
334
+ if (activeIndexFile?.endsWith('.lock')) {
335
+ const defaultIndexLock = normalizePath(
336
+ await this.execGit(['rev-parse', '--path-format=absolute', '--git-path', 'index.lock'])
337
+ )
324
338
 
325
- debugLog('Done updating default Git index lock again: %s', defaultIndexLock)
339
+ /**
340
+ * If the active index file is a non-default lockfile, we are committing with a pathspec
341
+ * without having explicitly run `git add`. In this case we need to also update the
342
+ * default index, otherwise there will be leftover diff after committing
343
+ */
344
+ if (activeIndexFile !== defaultIndexLock) {
345
+ debugLog('Updating default Git index again: %s', defaultIndexLock)
346
+ await this.execGit(['add', '--', ...addableFiles], {
347
+ env: {
348
+ GIT_INDEX_FILE: defaultIndexLock,
349
+ },
350
+ })
351
+ debugLog('Done updating default Git index lock: %s', defaultIndexLock)
352
+ }
326
353
  }
327
354
  }
328
355
 
package/lib/runAll.js CHANGED
@@ -302,12 +302,23 @@ export const runAll = async (
302
302
  return ctx
303
303
  }
304
304
 
305
+ // Chunk matched files for better Windows compatibility
306
+ /** @type {import('./getStagedFiles.js').StagedFile[][]} */
307
+ const matchedFileChunks = chunkFiles({
308
+ // matched files are relative to `cwd`, not `topLevelDir`, when `relative` is used
309
+ baseDir: cwd,
310
+ files: Array.from(matchedFiles),
311
+ maxArgLength,
312
+ relative: false,
313
+ })
314
+
305
315
  const git = new GitWorkflow({
306
316
  allowEmpty,
307
317
  diff,
308
318
  diffFilter,
309
319
  failOnChanges,
310
320
  gitConfigDir,
321
+ matchedFileChunks,
311
322
  topLevelDir,
312
323
  })
313
324
 
@@ -328,7 +339,7 @@ export const runAll = async (
328
339
  skip: () => listrTasks.every((task) => task.skip()),
329
340
  },
330
341
  {
331
- title: 'Updating Git index again...',
342
+ title: 'Staging changes from tasks...',
332
343
  task: (ctx) => git.updateIndex(ctx),
333
344
  skip: updateIndexSkipped,
334
345
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "17.0.5",
3
+ "version": "17.0.7",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,6 +18,10 @@
18
18
  "funding": {
19
19
  "url": "https://opencollective.com/lint-staged"
20
20
  },
21
+ "publishConfig": {
22
+ "provenance": true,
23
+ "access": "public"
24
+ },
21
25
  "engines": {
22
26
  "node": ">=22.22.1"
23
27
  },
@@ -51,31 +55,31 @@
51
55
  "listr2": "^10.2.1",
52
56
  "picomatch": "^4.0.4",
53
57
  "string-argv": "^0.3.2",
54
- "tinyexec": "^1.1.2"
58
+ "tinyexec": "^1.2.4"
55
59
  },
56
60
  "optionalDependencies": {
57
- "yaml": "^2.8.4"
61
+ "yaml": "^2.9.0"
58
62
  },
59
63
  "devDependencies": {
60
64
  "@changesets/changelog-github": "0.7.0",
61
65
  "@changesets/cli": "2.31.0",
62
- "@commitlint/cli": "21.0.0",
63
- "@commitlint/config-conventional": "21.0.0",
66
+ "@commitlint/cli": "21.0.2",
67
+ "@commitlint/config-conventional": "21.0.2",
64
68
  "@eslint/js": "10.0.1",
65
- "@vitest/coverage-istanbul": "4.1.5",
66
- "@vitest/eslint-plugin": "1.6.17",
69
+ "@vitest/coverage-istanbul": "4.1.7",
70
+ "@vitest/eslint-plugin": "1.6.18",
67
71
  "consolemock": "1.1.0",
68
72
  "cross-env": "10.1.0",
69
- "eslint": "10.3.0",
73
+ "eslint": "10.4.1",
70
74
  "eslint-config-prettier": "10.1.8",
71
75
  "eslint-plugin-n": "18.0.1",
72
- "eslint-plugin-prettier": "5.5.5",
76
+ "eslint-plugin-prettier": "5.5.6",
73
77
  "eslint-plugin-simple-import-sort": "13.0.0",
74
78
  "husky": "9.1.7",
75
79
  "mock-stdin": "1.0.0",
76
80
  "prettier": "3.8.3",
77
- "semver": "7.8.0",
78
- "vitest": "4.1.5"
81
+ "semver": "7.8.1",
82
+ "vitest": "4.1.7"
79
83
  },
80
84
  "keywords": [
81
85
  "lint",