lint-staged 11.2.0-beta.1 → 11.2.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/README.md +5 -4
- package/bin/lint-staged.js +16 -24
- package/lib/file.js +63 -0
- package/lib/gitWorkflow.js +137 -52
- package/lib/index.js +16 -8
- package/lib/messages.js +12 -10
- package/lib/resolveGitRepo.js +2 -2
- package/lib/runAll.js +16 -17
- package/lib/state.js +18 -20
- package/lib/symbols.js +4 -0
- package/lib/validateOptions.js +1 -5
- package/package.json +1 -1
- package/lib/unlink.js +0 -22
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-
|
|
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-
|
|
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
|
```
|
package/bin/lint-staged.js
CHANGED
|
@@ -22,22 +22,19 @@ require('please-upgrade-node')(
|
|
|
22
22
|
})
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
const
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
77
|
+
const cmdlineOptions = cmdline.opts()
|
|
85
78
|
|
|
86
79
|
const options = {
|
|
87
|
-
allowEmpty: !!
|
|
88
|
-
concurrent: JSON.parse(
|
|
89
|
-
configPath:
|
|
90
|
-
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
shell:
|
|
96
|
-
|
|
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
|
+
}
|
package/lib/gitWorkflow.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
199
|
+
const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
|
|
141
200
|
const files = processRenames(this.partiallyStagedFiles)
|
|
142
|
-
await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output',
|
|
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
|
-
/**
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
277
|
+
async restoreUnstagedChanges(ctx) {
|
|
219
278
|
debug('Restoring unstaged changes...')
|
|
220
|
-
const
|
|
279
|
+
const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
|
|
221
280
|
try {
|
|
222
|
-
await this.execGit(['apply', ...GIT_APPLY_ARGS,
|
|
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',
|
|
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
|
-
*
|
|
244
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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 {
|
|
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
|
|
80
|
+
stash = true,
|
|
77
81
|
verbose = false,
|
|
78
82
|
} = {},
|
|
79
83
|
logger = console
|
|
80
84
|
) => {
|
|
81
|
-
await validateOptions({ shell
|
|
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
|
|
32
|
-
const reason = hasInitialCommit ? '`--no-
|
|
33
|
-
return yellow(`${warning}
|
|
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
|
-
|
|
83
|
+
skippingBackup,
|
|
82
84
|
TASK_ERROR,
|
|
83
85
|
}
|
package/lib/resolveGitRepo.js
CHANGED
|
@@ -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,7 +21,7 @@ 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
|
|
24
|
+
const file = (await readFile(defaultDir)).toString()
|
|
25
25
|
return path.resolve(gitDir, file.replace(/^gitdir: /, '')).trim()
|
|
26
26
|
}
|
|
27
27
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
91
|
-
ctx.
|
|
92
|
-
ctx.
|
|
93
|
-
|
|
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: '
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/lib/validateOptions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { promises: fs, constants } = require('fs')
|
|
2
2
|
|
|
3
|
-
const {
|
|
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
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
|