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 +10 -4
- package/bin/lint-staged.js +2 -0
- package/lib/file.js +9 -27
- package/lib/getStagedFiles.js +9 -2
- package/lib/gitWorkflow.js +157 -126
- package/lib/index.js +11 -6
- package/lib/resolveGitRepo.js +4 -4
- package/lib/runAll.js +36 -14
- package/package.json +1 -1
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
|
|
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
|
-
-
|
|
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:
|
|
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
|
|
package/bin/lint-staged.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
package/lib/getStagedFiles.js
CHANGED
|
@@ -4,8 +4,15 @@ const execGit = require('./execGit')
|
|
|
4
4
|
|
|
5
5
|
module.exports = async function getStagedFiles(options) {
|
|
6
6
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
}
|
package/lib/gitWorkflow.js
CHANGED
|
@@ -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 {
|
|
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
|
|
19
|
-
|
|
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
|
-
*
|
|
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
|
|
114
|
-
|
|
115
|
-
const
|
|
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(
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
|
167
|
+
async prepare(ctx, stash) {
|
|
127
168
|
try {
|
|
128
169
|
debug('Backing up original state...')
|
|
129
170
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
await this.
|
|
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 =
|
|
138
|
-
.split('\n')
|
|
139
|
-
.filter(Boolean)
|
|
140
|
-
.map(file => path.resolve(this.gitDir, file))
|
|
190
|
+
this.deletedFiles = await this.getDeletedFiles()
|
|
141
191
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
await this.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
150
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
249
|
+
ctx.gitApplyEmptyCommitError = true
|
|
194
250
|
handleError(new Error('Prevented an empty git commit!'), ctx)
|
|
195
251
|
}
|
|
252
|
+
}
|
|
196
253
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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',
|
|
259
|
-
|
|
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
|
-
//
|
|
265
|
-
|
|
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
|
|
311
|
+
async cleanup(ctx) {
|
|
276
312
|
try {
|
|
277
313
|
debug('Dropping backup stash...')
|
|
278
|
-
await
|
|
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.
|
|
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
|
-
|
|
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!')
|
package/lib/resolveGitRepo.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
35
|
-
//
|
|
36
|
-
debugLog('
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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 =>
|
|
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.
|
|
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.
|
|
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
|
|