-
-
Notifications
You must be signed in to change notification settings - Fork 412
/
runAll.js
324 lines (285 loc) Β· 10.7 KB
/
runAll.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
/** @typedef {import('./index').Logger} Logger */
import path from 'node:path'
import chalk from 'chalk'
import debug from 'debug'
import { Listr } from 'listr2'
import { chunkFiles } from './chunkFiles.js'
import { execGit } from './execGit.js'
import { generateTasks } from './generateTasks.js'
import { getRenderer } from './getRenderer.js'
import { getStagedFiles } from './getStagedFiles.js'
import { GitWorkflow } from './gitWorkflow.js'
import { groupFilesByConfig } from './groupFilesByConfig.js'
import { makeCmdTasks } from './makeCmdTasks.js'
import {
DEPRECATED_GIT_ADD,
FAILED_GET_STAGED_FILES,
NOT_GIT_REPO,
NO_STAGED_FILES,
NO_TASKS,
SKIPPED_GIT_ERROR,
skippingBackup,
} from './messages.js'
import { normalizePath } from './normalizePath.js'
import { resolveGitRepo } from './resolveGitRepo.js'
import {
applyModificationsSkipped,
cleanupEnabled,
cleanupSkipped,
getInitialState,
hasPartiallyStagedFiles,
restoreOriginalStateEnabled,
restoreOriginalStateSkipped,
restoreUnstagedChangesSkipped,
} from './state.js'
import { GitRepoError, GetStagedFilesError, GitError, ConfigNotFoundError } from './symbols.js'
import { searchConfigs } from './searchConfigs.js'
const debugLog = debug('lint-staged:runAll')
const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ctx })
/**
* Executes all tasks and either resolves or rejects the promise
*
* @param {object} options
* @param {boolean} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
* @param {Object} [options.configObject] - Explicit config object from the js API
* @param {string} [options.configPath] - Explicit path to a config file
* @param {string} [options.cwd] - Current working directory
* @param {boolean} [options.debug] - Enable debug mode
* @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files
* @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
* @param {number} [options.maxArgLength] - Maximum argument string length
* @param {boolean} [options.quiet] - Disable lint-stagedβs own console output
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
* @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
* @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
* @param {Logger} logger
* @returns {Promise}
*/
export const runAll = async (
{
allowEmpty = false,
concurrent = true,
configObject,
configPath,
cwd,
debug = false,
diff,
diffFilter,
maxArgLength,
quiet = false,
relative = false,
shell = false,
// Stashing should be disabled by default when the `diff` option is used
stash = diff === undefined,
verbose = false,
},
logger = console
) => {
debugLog('Running all linter scripts...')
// Resolve relative CWD option
const hasExplicitCwd = !!cwd
cwd = hasExplicitCwd ? path.resolve(cwd) : process.cwd()
debugLog('Using working directory `%s`', cwd)
const ctx = getInitialState({ quiet })
const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
if (!gitDir) {
if (!quiet) ctx.output.push(NOT_GIT_REPO)
ctx.errors.add(GitRepoError)
throw createError(ctx)
}
// Test whether we have any commits or not.
// Stashing must be disabled with no initial commit.
const hasInitialCommit = await execGit(['log', '-1'], { cwd: gitDir })
.then(() => true)
.catch(() => false)
// Lint-staged will create a backup stash only when there's an initial commit,
// and when using the default list of staged files by default
ctx.shouldBackup = hasInitialCommit && stash
if (!ctx.shouldBackup) {
logger.warn(skippingBackup(hasInitialCommit, diff))
}
const files = await getStagedFiles({ cwd: gitDir, diff, diffFilter })
if (!files) {
if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
ctx.errors.add(GetStagedFilesError)
throw createError(ctx, GetStagedFilesError)
}
debugLog('Loaded list of staged files in git:\n%O', files)
// If there are no files avoid executing any lint-staged logic
if (files.length === 0) {
if (!quiet) ctx.output.push(NO_STAGED_FILES)
return ctx
}
const foundConfigs = await searchConfigs({ configObject, configPath, cwd, gitDir }, logger)
const numberOfConfigs = Object.keys(foundConfigs).length
// Throw if no configurations were found
if (numberOfConfigs === 0) {
ctx.errors.add(ConfigNotFoundError)
throw createError(ctx, ConfigNotFoundError)
}
const filesByConfig = await groupFilesByConfig({
configs: foundConfigs,
files,
singleConfigMode: configObject || configPath !== undefined,
})
const hasMultipleConfigs = numberOfConfigs > 1
// lint-staged 10 will automatically add modifications to index
// Warn user when their command includes `git add`
let hasDeprecatedGitAdd = false
const listrOptions = {
ctx,
exitOnError: false,
registerSignalListeners: false,
...getRenderer({ debug, quiet }, logger),
}
const listrTasks = []
// Set of all staged files that matched a task glob. Values in a set are unique.
const matchedFiles = new Set()
for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
const configName = configPath ? normalizePath(path.relative(cwd, configPath)) : 'Config object'
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
// Use actual cwd if it's specified, or there's only a single config file.
// Otherwise use the directory of the config file for each config group,
// to make sure tasks are separated from each other.
const groupCwd = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd
const chunkCount = stagedFileChunks.length
if (chunkCount > 1) {
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
}
for (const [index, files] of stagedFileChunks.entries()) {
const chunkListrTasks = await Promise.all(
generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
makeCmdTasks({
commands: task.commands,
cwd: groupCwd,
files: task.fileList,
gitDir,
shell,
verbose,
}).then((subTasks) => {
// Add files from task to match set
task.fileList.forEach((file) => {
// Make sure relative files are normalized to the
// group cwd, because other there might be identical
// relative filenames in the entire set.
const normalizedFile = path.isAbsolute(file)
? file
: normalizePath(path.join(groupCwd, file))
matchedFiles.add(normalizedFile)
})
hasDeprecatedGitAdd =
hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
const fileCount = task.fileList.length
return {
title: `${task.pattern}${chalk.dim(
` β ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`
)}`,
task: async (ctx, task) =>
task.newListr(
subTasks,
// Subtasks should not run in parallel, and should exit on error
{ concurrent: false, exitOnError: true }
),
skip: () => {
// Skip task when no files matched
if (fileCount === 0) {
return `${task.pattern}${chalk.dim(' β no files')}`
}
return false
},
}
})
)
)
listrTasks.push({
title:
`${configName}${chalk.dim(` β ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
(chunkCount > 1 ? chalk.dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
task: (ctx, task) => task.newListr(chunkListrTasks, { concurrent, exitOnError: true }),
skip: () => {
// Skip if the first step (backup) failed
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
// Skip chunk when no every task is skipped (due to no matches)
if (chunkListrTasks.every((task) => task.skip())) {
return `${configName}${chalk.dim(' β no tasks to run')}`
}
return false
},
})
}
}
if (hasDeprecatedGitAdd) {
logger.warn(DEPRECATED_GIT_ADD)
}
// If all of the configured tasks should be skipped
// avoid executing any lint-staged logic
if (listrTasks.every((task) => task.skip())) {
if (!quiet) ctx.output.push(NO_TASKS)
return ctx
}
// Chunk matched files for better Windows compatibility
const matchedFileChunks = chunkFiles({
// matched files are relative to `cwd`, not `gitDir`, when `relative` is used
baseDir: cwd,
files: Array.from(matchedFiles),
maxArgLength,
relative: false,
})
const git = new GitWorkflow({
allowEmpty,
gitConfigDir,
gitDir,
matchedFileChunks,
diff,
diffFilter,
})
const runner = new Listr(
[
{
title: 'Preparing lint-staged...',
task: (ctx) => git.prepare(ctx),
},
{
title: 'Hiding unstaged changes to partially staged files...',
task: (ctx) => git.hideUnstagedChanges(ctx),
enabled: hasPartiallyStagedFiles,
},
{
title: `Running tasks for staged files...`,
task: (ctx, task) => task.newListr(listrTasks, { concurrent }),
skip: () => listrTasks.every((task) => task.skip()),
},
{
title: 'Applying modifications from tasks...',
task: (ctx) => git.applyModifications(ctx),
skip: applyModificationsSkipped,
},
{
title: 'Restoring unstaged changes to partially staged files...',
task: (ctx) => git.restoreUnstagedChanges(ctx),
enabled: hasPartiallyStagedFiles,
skip: restoreUnstagedChangesSkipped,
},
{
title: 'Reverting to original state because of errors...',
task: (ctx) => git.restoreOriginalState(ctx),
enabled: restoreOriginalStateEnabled,
skip: restoreOriginalStateSkipped,
},
{
title: 'Cleaning up temporary files...',
task: (ctx) => git.cleanup(ctx),
enabled: cleanupEnabled,
skip: cleanupSkipped,
},
],
listrOptions
)
await runner.run()
if (ctx.errors.size > 0) {
throw createError(ctx)
}
return ctx
}