lint-staged 11.2.0-beta.1 → 11.2.3

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
@@ -65,7 +65,8 @@ Options:
65
65
  (default: false)
66
66
  -c, --config [path] path to configuration file, or - to read from stdin
67
67
  -d, --debug print additional debug information (default: false)
68
- --no-reset do not reset changes in case of errors
68
+ --no-stash disable the backup stash, and do not revert in case of
69
+ errors
69
70
  -p, --concurrent <parallel tasks> the number of tasks to run concurrently, or false to run
70
71
  tasks serially (default: true)
71
72
  -q, --quiet disable lint-staged’s own console output (default: false)
@@ -86,7 +87,7 @@ Options:
86
87
  - `false`: Run all tasks serially
87
88
  - `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible.
88
89
  - `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`.
89
- - **`--no-reset`**: 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.
90
+ - **`--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.
90
91
  - **`--quiet`**: Supress all CLI output, except from tasks.
91
92
  - **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
92
93
  - **`--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. To use a specific shell, use a path like `--shell "/bin/bash"`.
@@ -564,8 +565,8 @@ const success = await lintStaged({
564
565
  maxArgLength: null,
565
566
  quiet: false,
566
567
  relative: false,
567
- reset: true,
568
568
  shell: false
569
+ stash: true,
569
570
  verbose: false
570
571
  })
571
572
  ```
@@ -582,8 +583,8 @@ const success = await lintStaged({
582
583
  maxArgLength: null,
583
584
  quiet: false,
584
585
  relative: false,
585
- reset: true,
586
586
  shell: false,
587
+ stash: true,
587
588
  verbose: false,
588
589
  })
589
590
  ```
@@ -22,22 +22,19 @@ require('please-upgrade-node')(
22
22
  })
23
23
  )
24
24
 
25
- const { Command, Option } = require('commander')
25
+ const cmdline = require('commander')
26
26
  const debugLib = require('debug')
27
27
  const lintStaged = require('../lib')
28
28
  const { CONFIG_STDIN_ERROR } = require('../lib/messages')
29
29
 
30
30
  const debug = debugLib('lint-staged:bin')
31
31
 
32
- const program = new Command('lint-staged')
33
-
34
- program.version(pkg.version)
35
-
36
- program
32
+ cmdline
33
+ .version(pkg.version)
37
34
  .option('--allow-empty', 'allow empty commits when tasks revert all staged changes', false)
38
35
  .option('-c, --config [path]', 'path to configuration file, or - to read from stdin')
39
36
  .option('-d, --debug', 'print additional debug information', false)
40
- .option('--no-reset', 'do not reset changes in case of errors', false)
37
+ .option('--no-stash', 'disable the backup stash, and do not revert in case of errors', false)
41
38
  .option(
42
39
  '-p, --concurrent <parallel tasks>',
43
40
  'the number of tasks to run concurrently, or false to run tasks serially',
@@ -51,13 +48,9 @@ program
51
48
  'show task output even when tasks succeed; by default only failed output is shown',
52
49
  false
53
50
  )
51
+ .parse(process.argv)
54
52
 
55
- // Added for backwards-compatibility
56
- program.addOption(new Option('--no-stash').hideHelp())
57
-
58
- program.parse(process.argv)
59
-
60
- if (program.debug) {
53
+ if (cmdline.debug) {
61
54
  debugLib.enable('lint-staged*')
62
55
  }
63
56
 
@@ -81,20 +74,19 @@ const getMaxArgLength = () => {
81
74
  }
82
75
  }
83
76
 
84
- const programOpts = program.opts()
77
+ const cmdlineOptions = cmdline.opts()
85
78
 
86
79
  const options = {
87
- allowEmpty: !!programOpts.allowEmpty,
88
- concurrent: JSON.parse(programOpts.concurrent),
89
- configPath: programOpts.config,
90
- debug: !!programOpts.debug,
80
+ allowEmpty: !!cmdlineOptions.allowEmpty,
81
+ concurrent: JSON.parse(cmdlineOptions.concurrent),
82
+ configPath: cmdlineOptions.config,
83
+ debug: !!cmdlineOptions.debug,
91
84
  maxArgLength: getMaxArgLength() / 2,
92
- quiet: !!programOpts.quiet,
93
- relative: !!programOpts.relative,
94
- reset: !!programOpts.reset, // commander inverts `no-<x>` flags to `!x`
95
- shell: programOpts.shell /* Either a boolean or a string pointing to the shell */,
96
- stash: programOpts.stash /** kept for backwards-compatibility */,
97
- verbose: !!programOpts.verbose,
85
+ stash: !!cmdlineOptions.stash, // commander inverts `no-<x>` flags to `!x`
86
+ quiet: !!cmdlineOptions.quiet,
87
+ relative: !!cmdlineOptions.relative,
88
+ shell: cmdlineOptions.shell /* Either a boolean or a string pointing to the shell */,
89
+ verbose: !!cmdlineOptions.verbose,
98
90
  }
99
91
 
100
92
  debug('Options parsed from command-line:', options)
package/lib/file.js ADDED
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+
3
+ const debug = require('debug')('lint-staged:file')
4
+ const fs = require('fs')
5
+ const { promisify } = require('util')
6
+
7
+ const fsReadFile = promisify(fs.readFile)
8
+ const fsUnlink = promisify(fs.unlink)
9
+ const fsWriteFile = promisify(fs.writeFile)
10
+
11
+ /**
12
+ * Read contents of a file to buffer
13
+ * @param {String} filename
14
+ * @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist
15
+ * @returns {Promise<Buffer>}
16
+ */
17
+ const readFile = async (filename, ignoreENOENT = true) => {
18
+ debug('Reading file `%s`', filename)
19
+ try {
20
+ return await fsReadFile(filename)
21
+ } catch (error) {
22
+ if (ignoreENOENT && error.code === 'ENOENT') {
23
+ debug("File `%s` doesn't exist, ignoring...", filename)
24
+ return null // no-op file doesn't exist
25
+ } else {
26
+ throw error
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Remove a file
33
+ * @param {String} filename
34
+ * @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist
35
+ */
36
+ const unlink = async (filename, ignoreENOENT = true) => {
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
45
+ }
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Write buffer to file
51
+ * @param {String} filename
52
+ * @param {Buffer} buffer
53
+ */
54
+ const writeFile = async (filename, buffer) => {
55
+ debug('Writing file `%s`', filename)
56
+ await fsWriteFile(filename, buffer)
57
+ }
58
+
59
+ module.exports = {
60
+ readFile,
61
+ unlink,
62
+ writeFile,
63
+ }
@@ -1,21 +1,19 @@
1
1
  'use strict'
2
2
 
3
3
  const debug = require('debug')('lint-staged:git')
4
- const fs = require('fs')
5
4
  const path = require('path')
6
- const { promisify } = require('util')
7
5
 
8
6
  const execGit = require('./execGit')
7
+ const { readFile, unlink, writeFile } = require('./file')
9
8
  const {
10
9
  GitError,
10
+ RestoreOriginalStateError,
11
11
  ApplyEmptyCommitError,
12
+ GetBackupStashError,
12
13
  HideUnstagedChangesError,
13
- RestoreOriginalStateError,
14
+ RestoreMergeStatusError,
14
15
  RestoreUnstagedChangesError,
15
16
  } = require('./symbols')
16
- const unlink = require('./unlink')
17
-
18
- const fsReadFile = promisify(fs.readFile)
19
17
 
20
18
  const MERGE_HEAD = 'MERGE_HEAD'
21
19
  const MERGE_MODE = 'MERGE_MODE'
@@ -43,8 +41,9 @@ const processRenames = (files, includeRenameFrom = true) =>
43
41
  return flattened
44
42
  }, [])
45
43
 
44
+ const STASH = 'lint-staged automatic backup'
45
+
46
46
  const PATCH_UNSTAGED = 'lint-staged_unstaged.patch'
47
- const PATCH_PARTIAL = 'lint-staged_partial.patch'
48
47
 
49
48
  const GIT_DIFF_ARGS = [
50
49
  '--binary', // support binary files
@@ -91,6 +90,69 @@ class GitWorkflow {
91
90
  return path.resolve(this.gitConfigDir, `./${filename}`)
92
91
  }
93
92
 
93
+ /**
94
+ * Get name of backup stash
95
+ */
96
+ async getBackupStash(ctx) {
97
+ const stashes = await this.execGit(['stash', 'list'])
98
+ const index = stashes.split('\n').findIndex((line) => line.includes(STASH))
99
+ if (index === -1) {
100
+ ctx.errors.add(GetBackupStashError)
101
+ throw new Error('lint-staged automatic backup is missing!')
102
+ }
103
+ return `refs/stash@{${index}}`
104
+ }
105
+
106
+ /**
107
+ * Get a list of unstaged deleted files
108
+ */
109
+ async getDeletedFiles() {
110
+ debug('Getting deleted files...')
111
+ const lsFiles = await this.execGit(['ls-files', '--deleted'])
112
+ const deletedFiles = lsFiles
113
+ .split('\n')
114
+ .filter(Boolean)
115
+ .map((file) => path.resolve(this.gitDir, file))
116
+ debug('Found deleted files:', deletedFiles)
117
+ return deletedFiles
118
+ }
119
+
120
+ /**
121
+ * Save meta information about ongoing git merge
122
+ */
123
+ async backupMergeStatus() {
124
+ debug('Backing up merge state...')
125
+ await Promise.all([
126
+ readFile(this.mergeHeadFilename).then((buffer) => (this.mergeHeadBuffer = buffer)),
127
+ readFile(this.mergeModeFilename).then((buffer) => (this.mergeModeBuffer = buffer)),
128
+ readFile(this.mergeMsgFilename).then((buffer) => (this.mergeMsgBuffer = buffer)),
129
+ ])
130
+ debug('Done backing up merge state!')
131
+ }
132
+
133
+ /**
134
+ * Restore meta information about ongoing git merge
135
+ */
136
+ async restoreMergeStatus(ctx) {
137
+ debug('Restoring merge state...')
138
+ try {
139
+ await Promise.all([
140
+ this.mergeHeadBuffer && writeFile(this.mergeHeadFilename, this.mergeHeadBuffer),
141
+ this.mergeModeBuffer && writeFile(this.mergeModeFilename, this.mergeModeBuffer),
142
+ this.mergeMsgBuffer && writeFile(this.mergeMsgFilename, this.mergeMsgBuffer),
143
+ ])
144
+ debug('Done restoring merge state!')
145
+ } catch (error) {
146
+ debug('Failed restoring merge state with error:')
147
+ debug(error)
148
+ handleError(
149
+ new Error('Merge state could not be restored due to an error!'),
150
+ ctx,
151
+ RestoreMergeStatusError
152
+ )
153
+ }
154
+ }
155
+
94
156
  /**
95
157
  * Get a list of all files with both staged and unstaged modifications.
96
158
  * Renames have special treatment, since the single status line includes
@@ -122,28 +184,44 @@ class GitWorkflow {
122
184
  }
123
185
 
124
186
  /**
125
- * Create a diff of partially staged files.
187
+ * Create a diff of partially staged files and backup stash if enabled.
126
188
  */
127
189
  async prepare(ctx) {
128
190
  try {
129
191
  debug('Backing up original state...')
130
192
 
131
- const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
132
- await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch])
133
-
134
193
  // Get a list of files with bot staged and unstaged changes.
135
194
  // Unstaged changes to these files should be hidden before the tasks run.
136
195
  this.partiallyStagedFiles = await this.getPartiallyStagedFiles()
137
196
 
138
197
  if (this.partiallyStagedFiles) {
139
198
  ctx.hasPartiallyStagedFiles = true
140
- const partialPatch = this.getHiddenFilepath(PATCH_PARTIAL)
199
+ const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
141
200
  const files = processRenames(this.partiallyStagedFiles)
142
- await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', partialPatch, '--', ...files])
201
+ await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
143
202
  } else {
144
203
  ctx.hasPartiallyStagedFiles = false
145
204
  }
146
205
 
206
+ /**
207
+ * If backup stash should be skipped, no need to continue
208
+ */
209
+ if (!ctx.shouldBackup) return
210
+
211
+ // When backup is enabled, the revert will clear ongoing merge status.
212
+ await this.backupMergeStatus()
213
+
214
+ // Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
215
+ // - in git versions =< 2.13.0 the `git stash --keep-index` option resurrects deleted files
216
+ // - git stash can't infer RD or MD states correctly, and will lose the deletion
217
+ this.deletedFiles = await this.getDeletedFiles()
218
+
219
+ // Save stash of all staged files.
220
+ // The `stash create` command creates a dangling commit without removing any files,
221
+ // and `stash store` saves it as an actual stash.
222
+ const hash = await this.execGit(['stash', 'create'])
223
+ await this.execGit(['stash', 'store', '--quiet', '--message', STASH, hash])
224
+
147
225
  debug('Done backing up original state!')
148
226
  } catch (error) {
149
227
  handleError(error, ctx)
@@ -166,19 +244,13 @@ class GitWorkflow {
166
244
  }
167
245
  }
168
246
 
169
- /** Add all task modifications to index for files that were staged before running. */
247
+ /**
248
+ * Applies back task modifications, and unstaged changes hidden in the stash.
249
+ * In case of a merge-conflict retry with 3-way merge.
250
+ */
170
251
  async applyModifications(ctx) {
171
252
  debug('Adding task modifications to index...')
172
253
 
173
- if (ctx.hasInitialCommit) {
174
- const stagedFilesAfterAdd = await this.execGit(['diff', 'HEAD'])
175
- if (!stagedFilesAfterAdd && !this.allowEmpty) {
176
- // Tasks reverted all staged changes and the commit would be empty
177
- // Throw error to stop commit unless `--allow-empty` was used
178
- handleError(new Error('Prevented an empty git commit!'), ctx, ApplyEmptyCommitError)
179
- }
180
- }
181
-
182
254
  // `matchedFileChunks` includes staged files that lint-staged originally detected and matched against a task.
183
255
  // Add only these files so any 3rd-party edits to other files won't be included in the commit.
184
256
  // These additions per chunk are run "serially" to prevent race conditions.
@@ -188,25 +260,12 @@ class GitWorkflow {
188
260
  }
189
261
 
190
262
  debug('Done adding task modifications to index!')
191
- }
192
-
193
- /**
194
- * Restore original HEAD state in case of errors
195
- */
196
- async restoreOriginalState(ctx) {
197
- try {
198
- debug('Restoring original state...')
199
- await this.execGit(['checkout', '--force', '--', '.'])
200
263
 
201
- const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
202
- const hasContents = !!(await fsReadFile(unstagedPatch)).toString()
203
- if (hasContents) {
204
- await this.execGit(['apply', ...GIT_APPLY_ARGS, unstagedPatch])
205
- }
206
-
207
- debug('Done restoring original state!')
208
- } catch (error) {
209
- handleError(error, ctx, RestoreOriginalStateError)
264
+ const stagedFilesAfterAdd = await this.execGit(['diff', '--name-only', '--cached'])
265
+ if (!stagedFilesAfterAdd && !this.allowEmpty) {
266
+ // Tasks reverted all staged changes and the commit would be empty
267
+ // Throw error to stop commit unless `--allow-empty` was used
268
+ handleError(new Error('Prevented an empty git commit!'), ctx, ApplyEmptyCommitError)
210
269
  }
211
270
  }
212
271
 
@@ -215,18 +274,18 @@ class GitWorkflow {
215
274
  * this is probably because of conflicts between new task modifications.
216
275
  * 3-way merge usually fixes this, and in case it doesn't we should just give up and throw.
217
276
  */
218
- async restorePartialChanges(ctx) {
277
+ async restoreUnstagedChanges(ctx) {
219
278
  debug('Restoring unstaged changes...')
220
- const partialPatch = this.getHiddenFilepath(PATCH_PARTIAL)
279
+ const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
221
280
  try {
222
- await this.execGit(['apply', ...GIT_APPLY_ARGS, partialPatch])
281
+ await this.execGit(['apply', ...GIT_APPLY_ARGS, unstagedPatch])
223
282
  } catch (applyError) {
224
283
  debug('Error while restoring changes:')
225
284
  debug(applyError)
226
285
  debug('Retrying with 3-way merge')
227
286
  try {
228
287
  // Retry with a 3-way merge if normal apply fails
229
- await this.execGit(['apply', ...GIT_APPLY_ARGS, '--3way', partialPatch])
288
+ await this.execGit(['apply', ...GIT_APPLY_ARGS, '--3way', unstagedPatch])
230
289
  } catch (threeWayApplyError) {
231
290
  debug('Error while restoring unstaged changes using 3-way merge:')
232
291
  debug(threeWayApplyError)
@@ -240,14 +299,40 @@ class GitWorkflow {
240
299
  }
241
300
 
242
301
  /**
243
- * Drop the created diff file after everything has run.
244
- * Won't throw if the file has already been deleted (theoretically).
302
+ * Restore original HEAD state in case of errors
303
+ */
304
+ async restoreOriginalState(ctx) {
305
+ try {
306
+ debug('Restoring original state...')
307
+ await this.execGit(['reset', '--hard', 'HEAD'])
308
+ await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash(ctx)])
309
+
310
+ // Restore meta information about ongoing git merge
311
+ await this.restoreMergeStatus(ctx)
312
+
313
+ // If stashing resurrected deleted files, clean them out
314
+ await Promise.all(this.deletedFiles.map((file) => unlink(file)))
315
+
316
+ // Clean out patch
317
+ await unlink(this.getHiddenFilepath(PATCH_UNSTAGED))
318
+
319
+ debug('Done restoring original state!')
320
+ } catch (error) {
321
+ handleError(error, ctx, RestoreOriginalStateError)
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Drop the created stashes after everything has run
245
327
  */
246
- async cleanup() {
247
- debug('Removing temp files...')
248
- await unlink(this.getHiddenFilepath(PATCH_UNSTAGED))
249
- await unlink(this.getHiddenFilepath(PATCH_PARTIAL))
250
- debug('Done removing temp files!')
328
+ async cleanup(ctx) {
329
+ try {
330
+ debug('Dropping backup stash...')
331
+ await this.execGit(['stash', 'drop', '--quiet', await this.getBackupStash(ctx)])
332
+ debug('Done dropping backup stash!')
333
+ } catch (error) {
334
+ handleError(error, ctx)
335
+ }
251
336
  }
252
337
  }
253
338
 
package/lib/index.js CHANGED
@@ -4,10 +4,15 @@ const { cosmiconfig } = require('cosmiconfig')
4
4
  const debugLog = require('debug')('lint-staged')
5
5
  const stringifyObject = require('stringify-object')
6
6
 
7
- const { PREVENTED_EMPTY_COMMIT, GIT_ERROR } = require('./messages')
7
+ const { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } = require('./messages')
8
8
  const printTaskOutput = require('./printTaskOutput')
9
9
  const runAll = require('./runAll')
10
- const { ApplyEmptyCommitError, ConfigNotFoundError, GitError } = require('./symbols')
10
+ const {
11
+ ApplyEmptyCommitError,
12
+ ConfigNotFoundError,
13
+ GetBackupStashError,
14
+ GitError,
15
+ } = require('./symbols')
11
16
  const validateConfig = require('./validateConfig')
12
17
  const validateOptions = require('./validateOptions')
13
18
 
@@ -53,8 +58,8 @@ const loadConfig = (configPath) => {
53
58
  * @param {number} [options.maxArgLength] - Maximum argument string length
54
59
  * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
55
60
  * @param {boolean} [options.relative] - Pass relative filepaths to tasks
56
- * @param {boolean} [options.reset] - Reset changes in case of errors
57
61
  * @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
62
+ * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
58
63
  * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
59
64
  * @param {Logger} [logger]
60
65
  *
@@ -71,14 +76,13 @@ const lintStaged = async (
71
76
  maxArgLength,
72
77
  quiet = false,
73
78
  relative = false,
74
- reset = true,
75
79
  shell = false,
76
- stash, // kept for backwards-compatibility
80
+ stash = true,
77
81
  verbose = false,
78
82
  } = {},
79
83
  logger = console
80
84
  ) => {
81
- await validateOptions({ shell, stash }, logger)
85
+ await validateOptions({ shell }, logger)
82
86
 
83
87
  debugLog('Loading config using `cosmiconfig`')
84
88
 
@@ -122,8 +126,8 @@ const lintStaged = async (
122
126
  maxArgLength,
123
127
  quiet,
124
128
  relative,
125
- reset: reset || !!stash,
126
129
  shell,
130
+ stash,
127
131
  verbose,
128
132
  },
129
133
  logger
@@ -136,8 +140,12 @@ const lintStaged = async (
136
140
  const { ctx } = runAllError
137
141
  if (ctx.errors.has(ApplyEmptyCommitError)) {
138
142
  logger.warn(PREVENTED_EMPTY_COMMIT)
139
- } else if (ctx.errors.has(GitError)) {
143
+ } else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
140
144
  logger.error(GIT_ERROR)
145
+ if (ctx.shouldBackup) {
146
+ // No sense to show this if the backup stash itself is missing.
147
+ logger.error(RESTORE_STASH_EXAMPLE)
148
+ }
141
149
  }
142
150
 
143
151
  printTaskOutput(ctx, logger)
package/lib/messages.js CHANGED
@@ -28,9 +28,9 @@ const NO_STAGED_FILES = `${info} No staged files found.`
28
28
 
29
29
  const NO_TASKS = `${info} No staged files match any configured task.`
30
30
 
31
- const skippingReset = (hasInitialCommit) => {
32
- const reason = hasInitialCommit ? '`--no-reset` was used' : 'there’s no initial commit yet'
33
- return yellow(`${warning} Changes won't be reset because ${reason}.\n`)
31
+ const skippingBackup = (hasInitialCommit) => {
32
+ const reason = hasInitialCommit ? '`--no-stash` was used' : 'there’s no initial commit yet'
33
+ return yellow(`${warning} Skipping backup because ${reason}.\n`)
34
34
  }
35
35
 
36
36
  const DEPRECATED_GIT_ADD = yellow(
@@ -38,11 +38,6 @@ const DEPRECATED_GIT_ADD = yellow(
38
38
  `
39
39
  )
40
40
 
41
- const DEPRECATED_NO_STASH = yellow(
42
- `${warning} The \`--no-stash\` option has been renamed to \`--no-reset\`.
43
- `
44
- )
45
-
46
41
  const TASK_ERROR = 'Skipped because of errors from tasks.'
47
42
 
48
43
  const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.'
@@ -62,13 +57,19 @@ const PREVENTED_EMPTY_COMMIT = `
62
57
  Use the --allow-empty option to continue, or check your task configuration`)}
63
58
  `
64
59
 
60
+ const RESTORE_STASH_EXAMPLE = ` Any lost modifications can be restored from a git stash:
61
+
62
+ > git stash list
63
+ stash@{0}: automatic lint-staged backup
64
+ > git stash apply --index stash@{0}
65
+ `
66
+
65
67
  const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.'
66
68
 
67
69
  module.exports = {
68
70
  CONFIG_STDIN_ERROR,
69
71
  configurationError,
70
72
  DEPRECATED_GIT_ADD,
71
- DEPRECATED_NO_STASH,
72
73
  FAILED_GET_STAGED_FILES,
73
74
  GIT_ERROR,
74
75
  incorrectBraces,
@@ -77,7 +78,8 @@ module.exports = {
77
78
  NO_TASKS,
78
79
  NOT_GIT_REPO,
79
80
  PREVENTED_EMPTY_COMMIT,
81
+ RESTORE_STASH_EXAMPLE,
80
82
  SKIPPED_GIT_ERROR,
81
- skippingReset,
83
+ skippingBackup,
82
84
  TASK_ERROR,
83
85
  }
@@ -7,9 +7,9 @@ const path = require('path')
7
7
  const { promisify } = require('util')
8
8
 
9
9
  const execGit = require('./execGit')
10
+ const { readFile } = require('./file')
10
11
 
11
12
  const fsLstat = promisify(fs.lstat)
12
- const fsReadFile = promisify(fs.readFile)
13
13
 
14
14
  /**
15
15
  * Resolve path to the .git directory, with special handling for
@@ -21,14 +21,29 @@ const resolveGitConfigDir = async (gitDir) => {
21
21
  // If .git is a directory, use it
22
22
  if (stats.isDirectory()) return defaultDir
23
23
  // Otherwise .git is a file containing path to real location
24
- const file = (await fsReadFile(defaultDir)).toString()
24
+ const file = (await readFile(defaultDir)).toString()
25
25
  return path.resolve(gitDir, file.replace(/^gitdir: /, '')).trim()
26
26
  }
27
27
 
28
+ const determineGitDir = (cwd, relativeDir) => {
29
+ // if relative dir and cwd have different endings normalize it
30
+ // this happens under windows, where normalize is unable to normalize git's output
31
+ if (relativeDir && relativeDir.endsWith(path.sep)) {
32
+ relativeDir = relativeDir.slice(0, -1)
33
+ }
34
+ if (relativeDir) {
35
+ // the current working dir is inside the git top-level directory
36
+ return normalize(cwd.substring(0, cwd.lastIndexOf(relativeDir)))
37
+ } else {
38
+ // the current working dir is the top-level git directory
39
+ return normalize(cwd)
40
+ }
41
+ }
42
+
28
43
  /**
29
44
  * Resolve git directory and possible submodule paths
30
45
  */
31
- const resolveGitRepo = async (cwd) => {
46
+ const resolveGitRepo = async (cwd = process.cwd()) => {
32
47
  try {
33
48
  debugLog('Resolving git repo from `%s`', cwd)
34
49
 
@@ -38,7 +53,10 @@ const resolveGitRepo = async (cwd) => {
38
53
  debugLog('Unset GIT_WORK_TREE (was `%s`)', process.env.GIT_WORK_TREE)
39
54
  delete process.env.GIT_WORK_TREE
40
55
 
41
- const gitDir = normalize(await execGit(['rev-parse', '--show-toplevel'], { cwd }))
56
+ // read the path of the current directory relative to the top-level directory
57
+ // don't read the toplevel directly, it will lead to an posix conform path on non posix systems (cygwin)
58
+ const gitRel = normalize(await execGit(['rev-parse', '--show-prefix']))
59
+ const gitDir = determineGitDir(normalize(cwd), gitRel)
42
60
  const gitConfigDir = normalize(await resolveGitConfigDir(gitDir))
43
61
 
44
62
  debugLog('Resolved git directory to be `%s`', gitDir)
@@ -52,3 +70,6 @@ const resolveGitRepo = async (cwd) => {
52
70
  }
53
71
 
54
72
  module.exports = resolveGitRepo
73
+
74
+ // exported for test
75
+ module.exports.determineGitDir = determineGitDir
package/lib/runAll.js CHANGED
@@ -19,7 +19,7 @@ const {
19
19
  NO_STAGED_FILES,
20
20
  NO_TASKS,
21
21
  SKIPPED_GIT_ERROR,
22
- skippingReset,
22
+ skippingBackup,
23
23
  } = require('./messages')
24
24
  const resolveGitRepo = require('./resolveGitRepo')
25
25
  const {
@@ -30,7 +30,7 @@ const {
30
30
  hasPartiallyStagedFiles,
31
31
  restoreOriginalStateEnabled,
32
32
  restoreOriginalStateSkipped,
33
- restorePartialChangesSkipped,
33
+ restoreUnstagedChangesSkipped,
34
34
  } = require('./state')
35
35
  const { GitRepoError, GetStagedFilesError, GitError } = require('./symbols')
36
36
 
@@ -48,8 +48,8 @@ const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ct
48
48
  * @param {number} [options.maxArgLength] - Maximum argument string length
49
49
  * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
50
50
  * @param {boolean} [options.relative] - Pass relative filepaths to tasks
51
- * @param {boolean} [options.reset] - Reset changes in case of errors
52
51
  * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
52
+ * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
53
53
  * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
54
54
  * @param {Logger} logger
55
55
  * @returns {Promise}
@@ -64,8 +64,8 @@ const runAll = async (
64
64
  maxArgLength,
65
65
  quiet = false,
66
66
  relative = false,
67
- reset = true,
68
67
  shell = false,
68
+ stash = true,
69
69
  verbose = false,
70
70
  },
71
71
  logger = console
@@ -87,11 +87,10 @@ const runAll = async (
87
87
  .then(() => true)
88
88
  .catch(() => false)
89
89
 
90
- // Lint-staged should reset changes when errors happen only when there's an initial commit
91
- ctx.hasInitialCommit = hasInitialCommit
92
- ctx.shouldReset = hasInitialCommit && reset
93
- if (!ctx.shouldReset) {
94
- logger.warn(skippingReset(hasInitialCommit))
90
+ // Lint-staged should create a backup stash only when there's an initial commit
91
+ ctx.shouldBackup = hasInitialCommit && stash
92
+ if (!ctx.shouldBackup) {
93
+ logger.warn(skippingBackup(hasInitialCommit))
95
94
  }
96
95
 
97
96
  const files = await getStagedFiles({ cwd: gitDir })
@@ -226,20 +225,20 @@ const runAll = async (
226
225
  skip: applyModificationsSkipped,
227
226
  },
228
227
  {
229
- title: 'Reverting because of errors...',
228
+ title: 'Restoring unstaged changes to partially staged files...',
229
+ task: (ctx) => git.restoreUnstagedChanges(ctx),
230
+ enabled: hasPartiallyStagedFiles,
231
+ skip: restoreUnstagedChangesSkipped,
232
+ },
233
+ {
234
+ title: 'Reverting to original state because of errors...',
230
235
  task: (ctx) => git.restoreOriginalState(ctx),
231
236
  enabled: restoreOriginalStateEnabled,
232
237
  skip: restoreOriginalStateSkipped,
233
238
  },
234
- {
235
- title: 'Restoring partial changes...',
236
- task: (ctx) => git.restorePartialChanges(ctx),
237
- enabled: hasPartiallyStagedFiles,
238
- skip: restorePartialChangesSkipped,
239
- },
240
239
  {
241
240
  title: 'Cleaning up...',
242
- task: () => git.cleanup(),
241
+ task: (ctx) => git.cleanup(ctx),
243
242
  enabled: cleanupEnabled,
244
243
  skip: cleanupSkipped,
245
244
  },
package/lib/state.js CHANGED
@@ -4,13 +4,14 @@ const { GIT_ERROR, TASK_ERROR } = require('./messages')
4
4
  const {
5
5
  ApplyEmptyCommitError,
6
6
  TaskError,
7
+ RestoreOriginalStateError,
7
8
  GitError,
8
9
  RestoreUnstagedChangesError,
9
10
  } = require('./symbols')
10
11
 
11
12
  const getInitialState = ({ quiet = false } = {}) => ({
12
13
  hasPartiallyStagedFiles: null,
13
- shouldReset: null,
14
+ shouldBackup: null,
14
15
  errors: new Set([]),
15
16
  output: [],
16
17
  quiet,
@@ -20,41 +21,34 @@ const hasPartiallyStagedFiles = (ctx) => ctx.hasPartiallyStagedFiles
20
21
 
21
22
  const applyModificationsSkipped = (ctx) => {
22
23
  // Always apply back unstaged modifications when skipping backup
23
- if (!ctx.shouldReset) return false
24
-
24
+ if (!ctx.shouldBackup) return false
25
25
  // Should be skipped in case of git errors
26
26
  if (ctx.errors.has(GitError)) {
27
27
  return GIT_ERROR
28
28
  }
29
-
30
29
  // Should be skipped when tasks fail
31
30
  if (ctx.errors.has(TaskError)) {
32
31
  return TASK_ERROR
33
32
  }
34
33
  }
35
34
 
36
- const restorePartialChangesSkipped = (ctx) => {
37
- // Should be skipped when entire state has already been restored
38
- if (restoreOriginalStateEnabled(ctx)) {
39
- return TASK_ERROR
40
- }
41
-
35
+ const restoreUnstagedChangesSkipped = (ctx) => {
42
36
  // Should be skipped in case of git errors
43
37
  if (ctx.errors.has(GitError)) {
44
38
  return GIT_ERROR
45
39
  }
46
- }
47
-
48
- const restoreOriginalStateEnabled = (ctx) => {
49
- if (ctx.shouldReset && ctx.errors.has(TaskError)) {
40
+ // Should be skipped when tasks fail
41
+ if (ctx.errors.has(TaskError)) {
50
42
  return TASK_ERROR
51
43
  }
52
-
53
- if (ctx.errors.has(ApplyEmptyCommitError)) {
54
- return true
55
- }
56
44
  }
57
45
 
46
+ const restoreOriginalStateEnabled = (ctx) =>
47
+ ctx.shouldBackup &&
48
+ (ctx.errors.has(TaskError) ||
49
+ ctx.errors.has(ApplyEmptyCommitError) ||
50
+ ctx.errors.has(RestoreUnstagedChangesError))
51
+
58
52
  const restoreOriginalStateSkipped = (ctx) => {
59
53
  // Should be skipped in case of unknown git errors
60
54
  if (
@@ -66,7 +60,7 @@ const restoreOriginalStateSkipped = (ctx) => {
66
60
  }
67
61
  }
68
62
 
69
- const cleanupEnabled = (ctx) => ctx.shouldReset
63
+ const cleanupEnabled = (ctx) => ctx.shouldBackup
70
64
 
71
65
  const cleanupSkipped = (ctx) => {
72
66
  // Should be skipped in case of unknown git errors
@@ -77,13 +71,17 @@ const cleanupSkipped = (ctx) => {
77
71
  ) {
78
72
  return GIT_ERROR
79
73
  }
74
+ // Should be skipped when reverting to original state fails
75
+ if (ctx.errors.has(RestoreOriginalStateError)) {
76
+ return GIT_ERROR
77
+ }
80
78
  }
81
79
 
82
80
  module.exports = {
83
81
  getInitialState,
84
82
  hasPartiallyStagedFiles,
85
83
  applyModificationsSkipped,
86
- restorePartialChangesSkipped,
84
+ restoreUnstagedChangesSkipped,
87
85
  restoreOriginalStateEnabled,
88
86
  restoreOriginalStateSkipped,
89
87
  cleanupEnabled,
package/lib/symbols.js CHANGED
@@ -2,24 +2,28 @@
2
2
 
3
3
  const ApplyEmptyCommitError = Symbol('ApplyEmptyCommitError')
4
4
  const ConfigNotFoundError = new Error('Config could not be found')
5
+ const GetBackupStashError = Symbol('GetBackupStashError')
5
6
  const GetStagedFilesError = Symbol('GetStagedFilesError')
6
7
  const GitError = Symbol('GitError')
7
8
  const GitRepoError = Symbol('GitRepoError')
8
9
  const HideUnstagedChangesError = Symbol('HideUnstagedChangesError')
9
10
  const InvalidOptionsError = new Error('Invalid Options')
10
11
  const RestoreMergeStatusError = Symbol('RestoreMergeStatusError')
12
+ const RestoreOriginalStateError = Symbol('RestoreOriginalStateError')
11
13
  const RestoreUnstagedChangesError = Symbol('RestoreUnstagedChangesError')
12
14
  const TaskError = Symbol('TaskError')
13
15
 
14
16
  module.exports = {
15
17
  ApplyEmptyCommitError,
16
18
  ConfigNotFoundError,
19
+ GetBackupStashError,
17
20
  GetStagedFilesError,
18
21
  GitError,
19
22
  GitRepoError,
20
23
  InvalidOptionsError,
21
24
  HideUnstagedChangesError,
22
25
  RestoreMergeStatusError,
26
+ RestoreOriginalStateError,
23
27
  RestoreUnstagedChangesError,
24
28
  TaskError,
25
29
  }
@@ -1,6 +1,6 @@
1
1
  const { promises: fs, constants } = require('fs')
2
2
 
3
- const { DEPRECATED_NO_STASH, invalidOption } = require('./messages')
3
+ const { invalidOption } = require('./messages')
4
4
  const { InvalidOptionsError } = require('./symbols')
5
5
 
6
6
  const debug = require('debug')('lint-staged:options')
@@ -25,10 +25,6 @@ const validateOptions = async (options = {}, logger) => {
25
25
  }
26
26
  }
27
27
 
28
- if (typeof options.stash === 'boolean') {
29
- logger.warn(DEPRECATED_NO_STASH)
30
- }
31
-
32
28
  debug('Validated options!')
33
29
  }
34
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "11.2.0-beta.1",
3
+ "version": "11.2.3",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/okonet/lint-staged",
package/lib/unlink.js DELETED
@@ -1,22 +0,0 @@
1
- 'use strict'
2
-
3
- const debug = require('debug')('lint-staged:file')
4
- const fs = require('fs')
5
- const { promisify } = require('util')
6
-
7
- const fsUnlink = promisify(fs.unlink)
8
-
9
- /**
10
- * Remove a file if it exists
11
- * @param {String} filename
12
- */
13
- const unlink = async (filename) => {
14
- debug('Removing file `%s`', filename)
15
- try {
16
- await fsUnlink(filename)
17
- } catch (error) {
18
- debug("File `%s` doesn't exist, ignoring...", filename)
19
- }
20
- }
21
-
22
- module.exports = unlink