1 |
|
2 |
|
3 | import path from 'node:path'
|
4 |
|
5 | import { dim } from 'colorette'
|
6 | import debug from 'debug'
|
7 | import { Listr } from 'listr2'
|
8 | import normalize from 'normalize-path'
|
9 |
|
10 | import { chunkFiles } from './chunkFiles.js'
|
11 | import { execGit } from './execGit.js'
|
12 | import { generateTasks } from './generateTasks.js'
|
13 | import { getRenderer } from './getRenderer.js'
|
14 | import { getStagedFiles } from './getStagedFiles.js'
|
15 | import { GitWorkflow } from './gitWorkflow.js'
|
16 | import { groupFilesByConfig } from './groupFilesByConfig.js'
|
17 | import { makeCmdTasks } from './makeCmdTasks.js'
|
18 | import {
|
19 | DEPRECATED_GIT_ADD,
|
20 | FAILED_GET_STAGED_FILES,
|
21 | NOT_GIT_REPO,
|
22 | NO_STAGED_FILES,
|
23 | NO_TASKS,
|
24 | SKIPPED_GIT_ERROR,
|
25 | skippingBackup,
|
26 | } from './messages.js'
|
27 | import { resolveGitRepo } from './resolveGitRepo.js'
|
28 | import {
|
29 | applyModificationsSkipped,
|
30 | cleanupEnabled,
|
31 | cleanupSkipped,
|
32 | getInitialState,
|
33 | hasPartiallyStagedFiles,
|
34 | restoreOriginalStateEnabled,
|
35 | restoreOriginalStateSkipped,
|
36 | restoreUnstagedChangesSkipped,
|
37 | } from './state.js'
|
38 | import { GitRepoError, GetStagedFilesError, GitError, ConfigNotFoundError } from './symbols.js'
|
39 | import { searchConfigs } from './searchConfigs.js'
|
40 |
|
41 | const debugLog = debug('lint-staged:runAll')
|
42 |
|
43 | const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ctx })
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | export const runAll = async (
|
67 | {
|
68 | allowEmpty = false,
|
69 | concurrent = true,
|
70 | configObject,
|
71 | configPath,
|
72 | cwd,
|
73 | debug = false,
|
74 | diff,
|
75 | diffFilter,
|
76 | maxArgLength,
|
77 | quiet = false,
|
78 | relative = false,
|
79 | shell = false,
|
80 | stash = true,
|
81 | verbose = false,
|
82 | },
|
83 | logger = console
|
84 | ) => {
|
85 | debugLog('Running all linter scripts...')
|
86 |
|
87 |
|
88 | const hasExplicitCwd = !!cwd
|
89 | cwd = hasExplicitCwd ? path.resolve(cwd) : process.cwd()
|
90 | debugLog('Using working directory `%s`', cwd)
|
91 |
|
92 | const ctx = getInitialState({ quiet })
|
93 |
|
94 | const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
|
95 | if (!gitDir) {
|
96 | if (!quiet) ctx.output.push(NOT_GIT_REPO)
|
97 | ctx.errors.add(GitRepoError)
|
98 | throw createError(ctx)
|
99 | }
|
100 |
|
101 |
|
102 |
|
103 | const hasInitialCommit = await execGit(['log', '-1'], { cwd: gitDir })
|
104 | .then(() => true)
|
105 | .catch(() => false)
|
106 |
|
107 |
|
108 |
|
109 | ctx.shouldBackup = hasInitialCommit && stash && diff === undefined
|
110 | if (!ctx.shouldBackup) {
|
111 | logger.warn(skippingBackup(hasInitialCommit, diff))
|
112 | }
|
113 |
|
114 | const files = await getStagedFiles({ cwd: gitDir, diff, diffFilter })
|
115 | if (!files) {
|
116 | if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
|
117 | ctx.errors.add(GetStagedFilesError)
|
118 | throw createError(ctx, GetStagedFilesError)
|
119 | }
|
120 | debugLog('Loaded list of staged files in git:\n%O', files)
|
121 |
|
122 |
|
123 | if (files.length === 0) {
|
124 | if (!quiet) ctx.output.push(NO_STAGED_FILES)
|
125 | return ctx
|
126 | }
|
127 |
|
128 | const foundConfigs = await searchConfigs({ configObject, configPath, cwd, gitDir }, logger)
|
129 | const numberOfConfigs = Object.keys(foundConfigs).length
|
130 |
|
131 |
|
132 | if (numberOfConfigs === 0) {
|
133 | ctx.errors.add(ConfigNotFoundError)
|
134 | throw createError(ctx, ConfigNotFoundError)
|
135 | }
|
136 |
|
137 | const filesByConfig = await groupFilesByConfig({
|
138 | configs: foundConfigs,
|
139 | files,
|
140 | singleConfigMode: configObject || configPath !== undefined,
|
141 | })
|
142 |
|
143 | const hasMultipleConfigs = numberOfConfigs > 1
|
144 |
|
145 |
|
146 |
|
147 | let hasDeprecatedGitAdd = false
|
148 |
|
149 | const listrOptions = {
|
150 | ctx,
|
151 | exitOnError: false,
|
152 | registerSignalListeners: false,
|
153 | ...getRenderer({ debug, quiet }),
|
154 | }
|
155 |
|
156 | const listrTasks = []
|
157 |
|
158 |
|
159 | const matchedFiles = new Set()
|
160 |
|
161 | for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
|
162 | const configName = configPath ? normalize(path.relative(cwd, configPath)) : 'Config object'
|
163 |
|
164 | const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
|
165 |
|
166 |
|
167 |
|
168 |
|
169 | const groupCwd = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd
|
170 |
|
171 | const chunkCount = stagedFileChunks.length
|
172 | if (chunkCount > 1) {
|
173 | debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
|
174 | }
|
175 |
|
176 | for (const [index, files] of stagedFileChunks.entries()) {
|
177 | const chunkListrTasks = await Promise.all(
|
178 | generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
|
179 | makeCmdTasks({
|
180 | commands: task.commands,
|
181 | cwd: groupCwd,
|
182 | files: task.fileList,
|
183 | gitDir,
|
184 | renderer: listrOptions.renderer,
|
185 | shell,
|
186 | verbose,
|
187 | }).then((subTasks) => {
|
188 |
|
189 | task.fileList.forEach((file) => {
|
190 |
|
191 |
|
192 |
|
193 | const normalizedFile = path.isAbsolute(file)
|
194 | ? file
|
195 | : normalize(path.join(groupCwd, file))
|
196 |
|
197 | matchedFiles.add(normalizedFile)
|
198 | })
|
199 |
|
200 | hasDeprecatedGitAdd =
|
201 | hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
|
202 |
|
203 | const fileCount = task.fileList.length
|
204 |
|
205 | return {
|
206 | title: `${task.pattern}${dim(` — ${fileCount} ${fileCount > 1 ? 'files' : 'file'}`)}`,
|
207 | task: async () =>
|
208 | new Listr(subTasks, {
|
209 |
|
210 |
|
211 | ...listrOptions,
|
212 | concurrent: false,
|
213 | exitOnError: true,
|
214 | }),
|
215 | skip: () => {
|
216 |
|
217 | if (fileCount === 0) {
|
218 | return `${task.pattern}${dim(' — no files')}`
|
219 | }
|
220 | return false
|
221 | },
|
222 | }
|
223 | })
|
224 | )
|
225 | )
|
226 |
|
227 | listrTasks.push({
|
228 | title:
|
229 | `${configName}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
|
230 | (chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''),
|
231 | task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }),
|
232 | skip: () => {
|
233 |
|
234 | if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
|
235 |
|
236 | if (chunkListrTasks.every((task) => task.skip())) {
|
237 | return `${configName}${dim(' — no tasks to run')}`
|
238 | }
|
239 | return false
|
240 | },
|
241 | })
|
242 | }
|
243 | }
|
244 |
|
245 | if (hasDeprecatedGitAdd) {
|
246 | logger.warn(DEPRECATED_GIT_ADD)
|
247 | }
|
248 |
|
249 |
|
250 |
|
251 | if (listrTasks.every((task) => task.skip())) {
|
252 | if (!quiet) ctx.output.push(NO_TASKS)
|
253 | return ctx
|
254 | }
|
255 |
|
256 |
|
257 | const matchedFileChunks = chunkFiles({
|
258 |
|
259 | baseDir: cwd,
|
260 | files: Array.from(matchedFiles),
|
261 | maxArgLength,
|
262 | relative: false,
|
263 | })
|
264 |
|
265 | const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks })
|
266 |
|
267 | const runner = new Listr(
|
268 | [
|
269 | {
|
270 | title: 'Preparing lint-staged...',
|
271 | task: (ctx) => git.prepare(ctx),
|
272 | },
|
273 | {
|
274 | title: 'Hiding unstaged changes to partially staged files...',
|
275 | task: (ctx) => git.hideUnstagedChanges(ctx),
|
276 | enabled: hasPartiallyStagedFiles,
|
277 | },
|
278 | {
|
279 | title: `Running tasks for staged files...`,
|
280 | task: () => new Listr(listrTasks, { ...listrOptions, concurrent }),
|
281 | skip: () => listrTasks.every((task) => task.skip()),
|
282 | },
|
283 | {
|
284 | title: 'Applying modifications from tasks...',
|
285 | task: (ctx) => git.applyModifications(ctx),
|
286 | skip: applyModificationsSkipped,
|
287 | },
|
288 | {
|
289 | title: 'Restoring unstaged changes to partially staged files...',
|
290 | task: (ctx) => git.restoreUnstagedChanges(ctx),
|
291 | enabled: hasPartiallyStagedFiles,
|
292 | skip: restoreUnstagedChangesSkipped,
|
293 | },
|
294 | {
|
295 | title: 'Reverting to original state because of errors...',
|
296 | task: (ctx) => git.restoreOriginalState(ctx),
|
297 | enabled: restoreOriginalStateEnabled,
|
298 | skip: restoreOriginalStateSkipped,
|
299 | },
|
300 | {
|
301 | title: 'Cleaning up temporary files...',
|
302 | task: (ctx) => git.cleanup(ctx),
|
303 | enabled: cleanupEnabled,
|
304 | skip: cleanupSkipped,
|
305 | },
|
306 | ],
|
307 | listrOptions
|
308 | )
|
309 |
|
310 | await runner.run()
|
311 |
|
312 | if (ctx.errors.size > 0) {
|
313 | throw createError(ctx)
|
314 | }
|
315 |
|
316 | return ctx
|
317 | }
|