lint-staged 10.0.8 → 10.1.1

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
@@ -60,13 +60,18 @@ Usage: lint-staged [options]
60
60
 
61
61
  Options:
62
62
  -V, --version output the version number
63
- --allow-empty allow empty commits when tasks undo all staged changes (default: false)
63
+ --allow-empty allow empty commits when tasks revert all staged changes
64
+ (default: false)
64
65
  -c, --config [path] path to configuration file
65
66
  -d, --debug print additional debug information (default: false)
66
- -p, --concurrent <parallel tasks> the number of tasks to run concurrently, or false to run tasks serially (default: true)
67
+ --no-stash disable the backup stash, and do not revert in case of
68
+ errors
69
+ -p, --concurrent <parallel tasks> the number of tasks to run concurrently, or false to run
70
+ tasks serially (default: true)
67
71
  -q, --quiet disable lint-staged’s own console output (default: false)
68
72
  -r, --relative pass relative filepaths to tasks (default: false)
69
- -x, --shell skip parsing of tasks for better shell support (default: false)
73
+ -x, --shell skip parsing of tasks for better shell support (default:
74
+ false)
70
75
  -h, --help output usage information
71
76
  ```
72
77
 
@@ -79,6 +84,7 @@ Options:
79
84
  - `false`: Run all tasks serially
80
85
  - `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible.
81
86
  - `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`.
87
+ - **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit.
82
88
  - **`--quiet`**: Supress all CLI output, except from tasks.
83
89
  - **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
84
90
  - **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option.
@@ -168,7 +174,7 @@ Pass arguments to your commands separated by space as you would do in the shell.
168
174
 
169
175
  ## Running multiple commands in a sequence
170
176
 
171
- You can run multiple commands in a sequence on every glob. To do so, pass an array of commands instead of a single one. This is useful for running autoformatting tools like `eslint --fix` or `stylefmt` but can be used for any arbitrary sequences.
177
+ You can run multiple commands in a sequence on every glob. To do so, pass an array of commands instead of a single one. This is useful for running autoformatting tools like `eslint --fix` or `stylefmt` but can be used for any arbitrary sequences.
172
178
 
173
179
  For example:
174
180
 
@@ -31,6 +31,7 @@ cmdline
31
31
  .option('--allow-empty', 'allow empty commits when tasks revert all staged changes', false)
32
32
  .option('-c, --config [path]', 'path to configuration file')
33
33
  .option('-d, --debug', 'print additional debug information', false)
34
+ .option('--no-stash', 'disable the backup stash, and do not revert in case of errors', false)
34
35
  .option(
35
36
  '-p, --concurrent <parallel tasks>',
36
37
  'the number of tasks to run concurrently, or false to run tasks serially',
@@ -71,6 +72,7 @@ const options = {
71
72
  configPath: cmdline.config,
72
73
  debug: !!cmdline.debug,
73
74
  maxArgLength: getMaxArgLength() / 2,
75
+ stash: !!cmdline.stash, // commander inverts `no-<x>` flags to `!x`
74
76
  quiet: !!cmdline.quiet,
75
77
  relative: !!cmdline.relative,
76
78
  shell: !!cmdline.shell
package/lib/file.js CHANGED
@@ -4,25 +4,10 @@ const debug = require('debug')('lint-staged:file')
4
4
  const fs = require('fs')
5
5
  const { promisify } = require('util')
6
6
 
7
- const fsAccess = promisify(fs.access)
8
7
  const fsReadFile = promisify(fs.readFile)
9
8
  const fsUnlink = promisify(fs.unlink)
10
9
  const fsWriteFile = promisify(fs.writeFile)
11
10
 
12
- /**
13
- * Check if a file exists. Returns the filename if exists.
14
- * @param {String} filename
15
- * @returns {String|Boolean}
16
- */
17
- const exists = async filename => {
18
- try {
19
- await fsAccess(filename)
20
- return filename
21
- } catch {
22
- return false
23
- }
24
- }
25
-
26
11
  /**
27
12
  * Read contents of a file to buffer
28
13
  * @param {String} filename
@@ -44,21 +29,19 @@ const readFile = async (filename, ignoreENOENT = true) => {
44
29
  }
45
30
 
46
31
  /**
47
- * Unlink a file if it exists
32
+ * Remove a file
48
33
  * @param {String} filename
49
34
  * @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist
50
35
  */
51
36
  const unlink = async (filename, ignoreENOENT = true) => {
52
- if (filename) {
53
- debug('Unlinking file `%s`', filename)
54
- try {
55
- await fsUnlink(filename)
56
- } catch (error) {
57
- if (ignoreENOENT && error.code === 'ENOENT') {
58
- debug("File `%s` doesn't exist, ignoring...", filename)
59
- } else {
60
- throw error
61
- }
37
+ debug('Removing file `%s`', filename)
38
+ try {
39
+ await fsUnlink(filename)
40
+ } catch (error) {
41
+ if (ignoreENOENT && error.code === 'ENOENT') {
42
+ debug("File `%s` doesn't exist, ignoring...", filename)
43
+ } else {
44
+ throw error
62
45
  }
63
46
  }
64
47
  }
@@ -74,7 +57,6 @@ const writeFile = async (filename, buffer) => {
74
57
  }
75
58
 
76
59
  module.exports = {
77
- exists,
78
60
  readFile,
79
61
  unlink,
80
62
  writeFile
@@ -4,8 +4,15 @@ const execGit = require('./execGit')
4
4
 
5
5
  module.exports = async function getStagedFiles(options) {
6
6
  try {
7
- const lines = await execGit(['diff', '--staged', '--diff-filter=ACMR', '--name-only'], options)
8
- return lines ? lines.split('\n') : []
7
+ // Docs for --diff-filter option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203
8
+ // Docs for -z option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z
9
+ const lines = await execGit(
10
+ ['diff', '--staged', '--diff-filter=ACMR', '--name-only', '-z'],
11
+ options
12
+ )
13
+ // With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to remove the last occurrence of `\u0000` before splitting
14
+ // eslint-disable-next-line no-control-regex
15
+ return lines ? lines.replace(/\u0000$/, '').split('\u0000') : []
9
16
  } catch {
10
17
  return null
11
18
  }
@@ -4,19 +4,47 @@ const debug = require('debug')('lint-staged:git')
4
4
  const path = require('path')
5
5
 
6
6
  const execGit = require('./execGit')
7
- const { exists, readFile, unlink, writeFile } = require('./file')
7
+ const { readFile, unlink, writeFile } = require('./file')
8
8
 
9
9
  const MERGE_HEAD = 'MERGE_HEAD'
10
10
  const MERGE_MODE = 'MERGE_MODE'
11
11
  const MERGE_MSG = 'MERGE_MSG'
12
12
 
13
+ // In git status, renames are presented as `from` -> `to`.
14
+ // When diffing, both need to be taken into account, but in some cases on the `to`.
15
+ const RENAME = / -> /
16
+
17
+ /**
18
+ * From list of files, split renames and flatten into two files `from` -> `to`.
19
+ * @param {string[]} files
20
+ * @param {Boolean} [includeRenameFrom=true] Whether or not to include the `from` renamed file, which is no longer on disk
21
+ */
22
+ const processRenames = (files, includeRenameFrom = true) =>
23
+ files.reduce((flattened, file) => {
24
+ if (RENAME.test(file)) {
25
+ const [from, to] = file.split(RENAME)
26
+ if (includeRenameFrom) flattened.push(from)
27
+ flattened.push(to)
28
+ } else {
29
+ flattened.push(file)
30
+ }
31
+ return flattened
32
+ }, [])
33
+
13
34
  const STASH = 'lint-staged automatic backup'
14
35
 
15
36
  const PATCH_UNSTAGED = 'lint-staged_unstaged.patch'
16
- const PATCH_UNTRACKED = 'lint-staged_untracked.patch'
17
37
 
18
- const GIT_APPLY_ARGS = ['apply', '-v', '--whitespace=nowarn', '--recount', '--unidiff-zero']
19
- const GIT_DIFF_ARGS = ['--binary', '--unified=0', '--no-color', '--no-ext-diff', '--patch']
38
+ const GIT_DIFF_ARGS = [
39
+ '--binary', // support binary files
40
+ '--unified=0', // do not add lines around diff for consistent behaviour
41
+ '--no-color', // disable colors for consistent behaviour
42
+ '--no-ext-diff', // disable external diff tools for consistent behaviour
43
+ '--src-prefix=a/', // force prefix for consistent behaviour
44
+ '--dst-prefix=b/', // force prefix for consistent behaviour
45
+ '--patch' // output a patch that can be applied
46
+ ]
47
+ const GIT_APPLY_ARGS = ['-v', '--whitespace=nowarn', '--recount', '--unidiff-zero']
20
48
 
21
49
  const handleError = (error, ctx) => {
22
50
  ctx.gitError = true
@@ -26,6 +54,7 @@ const handleError = (error, ctx) => {
26
54
  class GitWorkflow {
27
55
  constructor({ allowEmpty, gitConfigDir, gitDir, stagedFileChunks }) {
28
56
  this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: gitDir })
57
+ this.deletedFiles = []
29
58
  this.gitConfigDir = gitConfigDir
30
59
  this.gitDir = gitDir
31
60
  this.unstagedDiff = null
@@ -49,19 +78,6 @@ class GitWorkflow {
49
78
  return path.resolve(this.gitConfigDir, `./${filename}`)
50
79
  }
51
80
 
52
- /**
53
- * Check if patch file exists and has content.
54
- * @param {string} filename
55
- */
56
- async hasPatch(filename) {
57
- const resolved = this.getHiddenFilepath(filename)
58
- const pathIfExists = await exists(resolved)
59
- if (!pathIfExists) return false
60
- const buffer = await readFile(pathIfExists)
61
- const patch = buffer.toString().trim()
62
- return patch.length ? filename : false
63
- }
64
-
65
81
  /**
66
82
  * Get name of backup stash
67
83
  */
@@ -75,6 +91,20 @@ class GitWorkflow {
75
91
  return `stash@{${index}}`
76
92
  }
77
93
 
94
+ /**
95
+ * Get a list of unstaged deleted files
96
+ */
97
+ async getDeletedFiles() {
98
+ debug('Getting deleted files...')
99
+ const lsFiles = await this.execGit(['ls-files', '--deleted'])
100
+ const deletedFiles = lsFiles
101
+ .split('\n')
102
+ .filter(Boolean)
103
+ .map(file => path.resolve(this.gitDir, file))
104
+ debug('Found deleted files:', deletedFiles)
105
+ return deletedFiles
106
+ }
107
+
78
108
  /**
79
109
  * Save meta information about ongoing git merge
80
110
  */
@@ -108,56 +138,70 @@ class GitWorkflow {
108
138
  }
109
139
 
110
140
  /**
111
- * List and delete untracked files
141
+ * Get a list of all files with both staged and unstaged modifications.
142
+ * Renames have special treatment, since the single status line includes
143
+ * both the "from" and "to" filenames, where "from" is no longer on disk.
112
144
  */
113
- async cleanUntrackedFiles() {
114
- const lsFiles = await this.execGit(['ls-files', '--others', '--exclude-standard'])
115
- const untrackedFiles = lsFiles
145
+ async getPartiallyStagedFiles() {
146
+ debug('Getting partially staged files...')
147
+ const status = await this.execGit(['status', '--porcelain'])
148
+ const partiallyStaged = status
116
149
  .split('\n')
117
- .filter(Boolean)
118
- .map(file => path.resolve(this.gitDir, file))
119
- await Promise.all(untrackedFiles.map(file => unlink(file)))
150
+ .filter(line => {
151
+ /**
152
+ * See https://git-scm.com/docs/git-status#_short_format
153
+ * The first letter of the line represents current index status,
154
+ * and second the working tree
155
+ */
156
+ const [index, workingTree] = line
157
+ return index !== ' ' && workingTree !== ' ' && index !== '?' && workingTree !== '?'
158
+ })
159
+ .map(line => line.substr(3)) // Remove first three letters (index, workingTree, and a whitespace)
160
+ debug('Found partially staged files:', partiallyStaged)
161
+ return partiallyStaged.length ? partiallyStaged : null
120
162
  }
121
163
 
122
164
  /**
123
- * Create backup stashes, one of everything and one of only staged changes
124
- * Staged files are left in the index for running tasks
165
+ * Create a diff of partially staged files and backup stash if enabled.
125
166
  */
126
- async stashBackup(ctx) {
167
+ async prepare(ctx, stash) {
127
168
  try {
128
169
  debug('Backing up original state...')
129
170
 
130
- // the `git stash` clears metadata about a possible git merge
131
- // Manually check and backup if necessary
132
- await this.backupMergeStatus()
171
+ // Get a list of files with bot staged and unstaged changes.
172
+ // Unstaged changes to these files should be hidden before the tasks run.
173
+ this.partiallyStagedFiles = await this.getPartiallyStagedFiles()
174
+
175
+ if (this.partiallyStagedFiles) {
176
+ ctx.hasPartiallyStagedFiles = true
177
+ const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
178
+ const files = processRenames(this.partiallyStagedFiles)
179
+ await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
180
+ }
181
+
182
+ /**
183
+ * If backup stash should be skipped, no need to continue
184
+ */
185
+ if (!stash) return
133
186
 
134
187
  // Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
135
188
  // - in git versions =< 2.13.0 the `--keep-index` flag resurrects deleted files
136
189
  // - git stash can't infer RD or MD states correctly, and will lose the deletion
137
- this.deletedFiles = (await this.execGit(['ls-files', '--deleted']))
138
- .split('\n')
139
- .filter(Boolean)
140
- .map(file => path.resolve(this.gitDir, file))
190
+ this.deletedFiles = await this.getDeletedFiles()
141
191
 
142
- // Save stash of entire original state, including unstaged and untracked changes.
143
- // `--keep-index leaves only staged files on disk, for tasks.`
144
- await this.execGit(['stash', 'save', '--include-untracked', '--keep-index', STASH])
192
+ // the `git stash` clears metadata about a possible git merge
193
+ // Manually check and backup if necessary
194
+ await this.backupMergeStatus()
145
195
 
146
- // Restore meta information about ongoing git merge
196
+ // Save stash of entire original state, including unstaged and untracked changes
197
+ await this.execGit(['stash', 'save', '--include-untracked', STASH])
198
+ await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash()])
199
+
200
+ // Restore meta information about ongoing git merge, cleared by `git stash`
147
201
  await this.restoreMergeStatus()
148
202
 
149
- // There is a bug in git =< 2.13.0 where `--keep-index` resurrects deleted files.
150
- // These files should be listed and deleted before proceeding.
151
- await this.cleanUntrackedFiles()
152
-
153
- // Get a diff of unstaged changes by diffing the saved stash against what's left on disk.
154
- await this.execGit([
155
- 'diff',
156
- ...GIT_DIFF_ARGS,
157
- `--output=${this.getHiddenFilepath(PATCH_UNSTAGED)}`,
158
- await this.getBackupStash(ctx),
159
- '-R' // Show diff in reverse
160
- ])
203
+ // If stashing resurrected deleted files, clean them out
204
+ await Promise.all(this.deletedFiles.map(file => unlink(file)))
161
205
 
162
206
  debug('Done backing up original state!')
163
207
  } catch (error) {
@@ -168,83 +212,72 @@ class GitWorkflow {
168
212
  }
169
213
  }
170
214
 
215
+ /**
216
+ * Remove unstaged changes to all partially staged files, to avoid tasks from seeing them
217
+ */
218
+ async hideUnstagedChanges(ctx) {
219
+ try {
220
+ const files = processRenames(this.partiallyStagedFiles, false)
221
+ await this.execGit(['checkout', '--force', '--', ...files])
222
+ } catch (error) {
223
+ /**
224
+ * `git checkout --force` doesn't throw errors, so it shouldn't be possible to get here.
225
+ * If this does fail, the handleError method will set ctx.gitError and lint-staged will fail.
226
+ */
227
+ ctx.gitHideUnstagedChangesError = true
228
+ handleError(error, ctx)
229
+ }
230
+ }
231
+
171
232
  /**
172
233
  * Applies back task modifications, and unstaged changes hidden in the stash.
173
234
  * In case of a merge-conflict retry with 3-way merge.
174
235
  */
175
236
  async applyModifications(ctx) {
176
- const modifiedFiles = await this.execGit(['ls-files', '--modified'])
177
- if (modifiedFiles) {
178
- debug('Detected files modified by tasks:')
179
- debug(modifiedFiles)
180
- debug('Adding files to index...')
181
- await Promise.all(
182
- // stagedFileChunks includes staged files that lint-staged originally detected.
183
- // Add only these files so any 3rd-party edits to other files won't be included in the commit.
184
- this.stagedFileChunks.map(stagedFiles => this.execGit(['add', ...stagedFiles]))
185
- )
186
- debug('Done adding files to index!')
187
- }
188
-
189
- const modifiedFilesAfterAdd = await this.execGit(['status', '--porcelain'])
190
- if (!modifiedFilesAfterAdd && !this.allowEmpty) {
237
+ debug('Adding task modifications to index...')
238
+ await Promise.all(
239
+ // stagedFileChunks includes staged files that lint-staged originally detected.
240
+ // Add only these files so any 3rd-party edits to other files won't be included in the commit.
241
+ this.stagedFileChunks.map(stagedFiles => this.execGit(['add', '--', ...stagedFiles]))
242
+ )
243
+ debug('Done adding task modifications to index!')
244
+
245
+ const stagedFilesAfterAdd = await this.execGit(['diff', '--name-only', '--cached'])
246
+ if (!stagedFilesAfterAdd && !this.allowEmpty) {
191
247
  // Tasks reverted all staged changes and the commit would be empty
192
248
  // Throw error to stop commit unless `--allow-empty` was used
193
- ctx.gitApplyEmptyCommit = true
249
+ ctx.gitApplyEmptyCommitError = true
194
250
  handleError(new Error('Prevented an empty git commit!'), ctx)
195
251
  }
252
+ }
196
253
 
197
- // Restore unstaged changes by applying the diff back. If it at first fails,
198
- // this is probably because of conflicts between task modifications.
199
- // 3-way merge usually fixes this, and in case it doesn't we should just give up and throw.
200
- if (await this.hasPatch(PATCH_UNSTAGED)) {
201
- debug('Restoring unstaged changes...')
202
- const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
203
- try {
204
- await this.execGit([...GIT_APPLY_ARGS, unstagedPatch])
205
- } catch (error) {
206
- debug('Error while restoring changes:')
207
- debug(error)
208
- debug('Retrying with 3-way merge')
209
-
210
- try {
211
- // Retry with `--3way` if normal apply fails
212
- await this.execGit([...GIT_APPLY_ARGS, '--3way', unstagedPatch])
213
- } catch (error2) {
214
- debug('Error while restoring unstaged changes using 3-way merge:')
215
- debug(error2)
216
- ctx.gitApplyModificationsError = true
217
- handleError(
218
- new Error('Unstaged changes could not be restored due to a merge conflict!'),
219
- ctx
220
- )
221
- }
222
- }
223
- debug('Done restoring unstaged changes!')
224
- }
225
-
226
- // Restore untracked files by reading from the third commit associated with the backup stash
227
- // See https://stackoverflow.com/a/52357762
254
+ /**
255
+ * Restore unstaged changes to partially changed files. If it at first fails,
256
+ * this is probably because of conflicts between new task modifications.
257
+ * 3-way merge usually fixes this, and in case it doesn't we should just give up and throw.
258
+ */
259
+ async restoreUnstagedChanges(ctx) {
260
+ debug('Restoring unstaged changes...')
261
+ const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
228
262
  try {
229
- const backupStash = await this.getBackupStash(ctx)
230
- const untrackedPatch = this.getHiddenFilepath(PATCH_UNTRACKED)
231
- await this.execGit([
232
- 'show',
233
- ...GIT_DIFF_ARGS,
234
- '--format=%b',
235
- `--output=${untrackedPatch}`,
236
- `${backupStash}^3`
237
- ])
238
- if (await this.hasPatch(PATCH_UNTRACKED)) {
239
- await this.execGit([...GIT_APPLY_ARGS, untrackedPatch])
263
+ await this.execGit(['apply', ...GIT_APPLY_ARGS, unstagedPatch])
264
+ } catch (applyError) {
265
+ debug('Error while restoring changes:')
266
+ debug(applyError)
267
+ debug('Retrying with 3-way merge')
268
+ try {
269
+ // Retry with a 3-way merge if normal apply fails
270
+ await this.execGit(['apply', ...GIT_APPLY_ARGS, '--3way', unstagedPatch])
271
+ } catch (threeWayApplyError) {
272
+ debug('Error while restoring unstaged changes using 3-way merge:')
273
+ debug(threeWayApplyError)
274
+ ctx.gitRestoreUnstagedChangesError = true
275
+ handleError(
276
+ new Error('Unstaged changes could not be restored due to a merge conflict!'),
277
+ ctx
278
+ )
240
279
  }
241
- } catch (error) {
242
- ctx.gitRestoreUntrackedError = true
243
- handleError(error, ctx)
244
280
  }
245
-
246
- // If stashing resurrected deleted files, clean them out
247
- await Promise.all(this.deletedFiles.map(file => unlink(file)))
248
281
  }
249
282
 
250
283
  /**
@@ -253,16 +286,19 @@ class GitWorkflow {
253
286
  async restoreOriginalState(ctx) {
254
287
  try {
255
288
  debug('Restoring original state...')
256
- const backupStash = await this.getBackupStash(ctx)
257
289
  await this.execGit(['reset', '--hard', 'HEAD'])
258
- await this.execGit(['stash', 'apply', '--quiet', '--index', backupStash])
259
- debug('Done restoring original state!')
290
+ await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash(ctx)])
291
+
292
+ // Restore meta information about ongoing git merge
293
+ await this.restoreMergeStatus()
260
294
 
261
295
  // If stashing resurrected deleted files, clean them out
262
296
  await Promise.all(this.deletedFiles.map(file => unlink(file)))
263
297
 
264
- // Restore meta information about ongoing git merge
265
- await this.restoreMergeStatus()
298
+ // Clean out patch
299
+ if (this.partiallyStagedFiles) await unlink(PATCH_UNSTAGED)
300
+
301
+ debug('Done restoring original state!')
266
302
  } catch (error) {
267
303
  ctx.gitRestoreOriginalStateError = true
268
304
  handleError(error, ctx)
@@ -272,15 +308,10 @@ class GitWorkflow {
272
308
  /**
273
309
  * Drop the created stashes after everything has run
274
310
  */
275
- async dropBackup(ctx) {
311
+ async cleanup(ctx) {
276
312
  try {
277
313
  debug('Dropping backup stash...')
278
- await Promise.all([
279
- exists(this.getHiddenFilepath(PATCH_UNSTAGED)).then(unlink),
280
- exists(this.getHiddenFilepath(PATCH_UNTRACKED)).then(unlink)
281
- ])
282
- const backupStash = await this.getBackupStash(ctx)
283
- await this.execGit(['stash', 'drop', '--quiet', backupStash])
314
+ await this.execGit(['stash', 'drop', '--quiet', await this.getBackupStash(ctx)])
284
315
  debug('Done dropping backup stash!')
285
316
  } catch (error) {
286
317
  handleError(error, ctx)
package/lib/index.js CHANGED
@@ -46,12 +46,12 @@ function loadConfig(configPath) {
46
46
  * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
47
47
  * @param {object} [options.config] - Object with configuration for programmatic API
48
48
  * @param {string} [options.configPath] - Path to configuration file
49
+ * @param {boolean} [options.debug] - Enable debug mode
49
50
  * @param {number} [options.maxArgLength] - Maximum argument string length
51
+ * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
50
52
  * @param {boolean} [options.relative] - Pass relative filepaths to tasks
51
53
  * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
52
- * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
53
- * @param {boolean} [options.debug] - Enable debug mode
54
- * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
54
+ * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
55
55
  * @param {Logger} [logger]
56
56
  *
57
57
  * @returns {Promise<boolean>} Promise of whether the linting passed or failed
@@ -62,11 +62,12 @@ module.exports = async function lintStaged(
62
62
  concurrent = true,
63
63
  config: configObject,
64
64
  configPath,
65
+ debug = false,
65
66
  maxArgLength,
67
+ quiet = false,
66
68
  relative = false,
67
69
  shell = false,
68
- quiet = false,
69
- debug = false
70
+ stash = true
70
71
  } = {},
71
72
  logger = console
72
73
  ) {
@@ -92,9 +93,13 @@ module.exports = async function lintStaged(
92
93
  debugLog('lint-staged config:\n%O', config)
93
94
  }
94
95
 
96
+ // Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
97
+ debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
98
+ delete process.env.GIT_LITERAL_PATHSPECS
99
+
95
100
  try {
96
101
  await runAll(
97
- { allowEmpty, concurrent, config, debug, maxArgLength, quiet, relative, shell },
102
+ { allowEmpty, concurrent, config, debug, maxArgLength, stash, quiet, relative, shell },
98
103
  logger
99
104
  )
100
105
  debugLog('tasks were executed successfully!')
@@ -16,7 +16,7 @@ const fsLstat = promisify(fs.lstat)
16
16
  * submodules and worktrees
17
17
  */
18
18
  const resolveGitConfigDir = async gitDir => {
19
- const defaultDir = path.resolve(gitDir, '.git')
19
+ const defaultDir = normalize(path.join(gitDir, '.git'))
20
20
  const stats = await fsLstat(defaultDir)
21
21
  // If .git is a directory, use it
22
22
  if (stats.isDirectory()) return defaultDir
@@ -31,9 +31,9 @@ const resolveGitConfigDir = async gitDir => {
31
31
  const resolveGitRepo = async cwd => {
32
32
  try {
33
33
  debugLog('Resolving git repo from `%s`', cwd)
34
- // git cli uses GIT_DIR to fast track its response however it might be set to a different path
35
- // depending on where the caller initiated this from, hence clear GIT_DIR
36
- debugLog('Deleting GIT_DIR from env with value `%s`', process.env.GIT_DIR)
34
+
35
+ // Unset GIT_DIR before running any git operations in case it's pointing to an incorrect location
36
+ debugLog('Unset GIT_DIR (was `%s`)', process.env.GIT_DIR)
37
37
  delete process.env.GIT_DIR
38
38
 
39
39
  const gitDir = normalize(await execGit(['rev-parse', '--show-toplevel'], { cwd }))
package/lib/runAll.js CHANGED
@@ -41,14 +41,14 @@ const shouldSkipApplyModifications = ctx => {
41
41
 
42
42
  const shouldSkipRevert = ctx => {
43
43
  // Should be skipped in case of unknown git errors
44
- if (ctx.gitError && !ctx.gitApplyEmptyCommit && !ctx.gitApplyModificationsError) {
44
+ if (ctx.gitError && !ctx.gitApplyEmptyCommitError && !ctx.gitRestoreUnstagedChangesError) {
45
45
  return MESSAGES.GIT_ERROR
46
46
  }
47
47
  }
48
48
 
49
49
  const shouldSkipCleanup = ctx => {
50
50
  // Should be skipped in case of unknown git errors
51
- if (ctx.gitError && !ctx.gitApplyEmptyCommit && !ctx.gitApplyModificationsError) {
51
+ if (ctx.gitError && !ctx.gitApplyEmptyCommitError && !ctx.gitRestoreUnstagedChangesError) {
52
52
  return MESSAGES.GIT_ERROR
53
53
  }
54
54
  // Should be skipped when reverting to original state fails
@@ -62,20 +62,22 @@ const shouldSkipCleanup = ctx => {
62
62
  *
63
63
  * @param {object} options
64
64
  * @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
65
+ * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
65
66
  * @param {Object} [options.config] - Task configuration
66
67
  * @param {Object} [options.cwd] - Current working directory
68
+ * @param {boolean} [options.debug] - Enable debug mode
67
69
  * @param {number} [options.maxArgLength] - Maximum argument string length
70
+ * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
68
71
  * @param {boolean} [options.relative] - Pass relative filepaths to tasks
69
72
  * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
70
- * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
71
- * @param {boolean} [options.debug] - Enable debug mode
72
- * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
73
+ * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
73
74
  * @param {Logger} logger
74
75
  * @returns {Promise}
75
76
  */
76
77
  const runAll = async (
77
78
  {
78
79
  allowEmpty = false,
80
+ concurrent = true,
79
81
  config,
80
82
  cwd = process.cwd(),
81
83
  debug = false,
@@ -83,12 +85,18 @@ const runAll = async (
83
85
  quiet = false,
84
86
  relative = false,
85
87
  shell = false,
86
- concurrent = true
88
+ stash = true
87
89
  },
88
90
  logger = console
89
91
  ) => {
90
92
  debugLog('Running all linter scripts')
91
93
 
94
+ if (!stash) {
95
+ logger.warn(
96
+ `${symbols.warning} ${chalk.yellow('Skipping backup because `--no-stash` was used.')}`
97
+ )
98
+ }
99
+
92
100
  const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
93
101
  if (!gitDir) throw new Error('Current directory is not a git directory!')
94
102
 
@@ -98,8 +106,7 @@ const runAll = async (
98
106
 
99
107
  // If there are no files avoid executing any lint-staged logic
100
108
  if (files.length === 0) {
101
- logger.log('No staged files found.')
102
- return 'No tasks to run.'
109
+ return logger.log(`${symbols.info} No staged files found.`)
103
110
  }
104
111
 
105
112
  const stagedFileChunks = chunkFiles({ files, gitDir, maxArgLength, relative })
@@ -189,23 +196,38 @@ const runAll = async (
189
196
  [
190
197
  {
191
198
  title: 'Preparing...',
192
- task: ctx => git.stashBackup(ctx)
199
+ task: ctx => git.prepare(ctx, stash)
200
+ },
201
+ {
202
+ title: 'Hiding unstaged changes to partially staged files...',
203
+ task: ctx => git.hideUnstagedChanges(ctx),
204
+ enabled: ctx => ctx.hasPartiallyStagedFiles
193
205
  },
194
206
  ...listrTasks,
195
207
  {
196
208
  title: 'Applying modifications...',
197
209
  task: ctx => git.applyModifications(ctx),
210
+ // Always apply back unstaged modifications when skipping backup
211
+ skip: ctx => stash && shouldSkipApplyModifications(ctx)
212
+ },
213
+ {
214
+ title: 'Restoring unstaged changes to partially staged files...',
215
+ task: ctx => git.restoreUnstagedChanges(ctx),
216
+ enabled: ctx => ctx.hasPartiallyStagedFiles,
198
217
  skip: shouldSkipApplyModifications
199
218
  },
200
219
  {
201
- title: 'Reverting to original state...',
220
+ title: 'Reverting to original state because of errors...',
202
221
  task: ctx => git.restoreOriginalState(ctx),
203
- enabled: ctx => ctx.taskError || ctx.gitApplyEmptyCommit || ctx.gitApplyModificationsError,
222
+ enabled: ctx =>
223
+ stash &&
224
+ (ctx.taskError || ctx.gitApplyEmptyCommitError || ctx.gitRestoreUnstagedChangesError),
204
225
  skip: shouldSkipRevert
205
226
  },
206
227
  {
207
228
  title: 'Cleaning up...',
208
- task: ctx => git.dropBackup(ctx),
229
+ task: ctx => git.cleanup(ctx),
230
+ enabled: () => stash,
209
231
  skip: shouldSkipCleanup
210
232
  }
211
233
  ],
@@ -215,7 +237,7 @@ const runAll = async (
215
237
  try {
216
238
  await runner.run({})
217
239
  } catch (error) {
218
- if (error.context.gitApplyEmptyCommit) {
240
+ if (error.context.gitApplyEmptyCommitError) {
219
241
  logger.warn(`
220
242
  ${symbols.warning} ${chalk.yellow(`lint-staged prevented an empty git commit.
221
243
  Use the --allow-empty option to continue, or check your task configuration`)}
@@ -228,7 +250,7 @@ const runAll = async (
228
250
  `\n The initial commit is needed for lint-staged to work.
229
251
  Please use the --no-verify flag to skip running lint-staged.`
230
252
  )
231
- } else {
253
+ } else if (stash) {
232
254
  // No sense to show this if the backup stash itself is missing.
233
255
  logger.error(` Any lost modifications can be restored from a git stash:
234
256
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "10.0.8",
3
+ "version": "10.1.1",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/okonet/lint-staged",