UNPKG

8.03 kBJavaScriptView Raw
1/** @typedef {import('./index').Logger} Logger */
2
3import debug from 'debug'
4import { Listr } from 'listr2'
5
6import { chunkFiles } from './chunkFiles.js'
7import { execGit } from './execGit.js'
8import { generateTasks } from './generateTasks.js'
9import { getRenderer } from './getRenderer.js'
10import { getStagedFiles } from './getStagedFiles.js'
11import { GitWorkflow } from './gitWorkflow.js'
12import { makeCmdTasks } from './makeCmdTasks.js'
13import {
14 DEPRECATED_GIT_ADD,
15 FAILED_GET_STAGED_FILES,
16 NOT_GIT_REPO,
17 NO_STAGED_FILES,
18 NO_TASKS,
19 SKIPPED_GIT_ERROR,
20 skippingBackup,
21} from './messages.js'
22import { resolveGitRepo } from './resolveGitRepo.js'
23import {
24 applyModificationsSkipped,
25 cleanupEnabled,
26 cleanupSkipped,
27 getInitialState,
28 hasPartiallyStagedFiles,
29 restoreOriginalStateEnabled,
30 restoreOriginalStateSkipped,
31 restoreUnstagedChangesSkipped,
32} from './state.js'
33import { GitRepoError, GetStagedFilesError, GitError } from './symbols.js'
34
35const debugLog = debug('lint-staged:runAll')
36
37const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ctx })
38
39/**
40 * Executes all tasks and either resolves or rejects the promise
41 *
42 * @param {object} options
43 * @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
44 * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
45 * @param {Object} [options.config] - Task configuration
46 * @param {Object} [options.cwd] - Current working directory
47 * @param {boolean} [options.debug] - Enable debug mode
48 * @param {number} [options.maxArgLength] - Maximum argument string length
49 * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
50 * @param {boolean} [options.relative] - Pass relative filepaths to tasks
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 * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
54 * @param {Logger} logger
55 * @returns {Promise}
56 */
57export const runAll = async (
58 {
59 allowEmpty = false,
60 concurrent = true,
61 config,
62 cwd = process.cwd(),
63 debug = false,
64 maxArgLength,
65 quiet = false,
66 relative = false,
67 shell = false,
68 stash = true,
69 verbose = false,
70 },
71 logger = console
72) => {
73 debugLog('Running all linter scripts')
74
75 const ctx = getInitialState({ quiet })
76
77 const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
78 if (!gitDir) {
79 if (!quiet) ctx.output.push(NOT_GIT_REPO)
80 ctx.errors.add(GitRepoError)
81 throw createError(ctx)
82 }
83
84 // Test whether we have any commits or not.
85 // Stashing must be disabled with no initial commit.
86 const hasInitialCommit = await execGit(['log', '-1'], { cwd: gitDir })
87 .then(() => true)
88 .catch(() => false)
89
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))
94 }
95
96 const files = await getStagedFiles({ cwd: gitDir })
97 if (!files) {
98 if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
99 ctx.errors.add(GetStagedFilesError)
100 throw createError(ctx, GetStagedFilesError)
101 }
102 debugLog('Loaded list of staged files in git:\n%O', files)
103
104 // If there are no files avoid executing any lint-staged logic
105 if (files.length === 0) {
106 if (!quiet) ctx.output.push(NO_STAGED_FILES)
107 return ctx
108 }
109
110 const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
111 const chunkCount = stagedFileChunks.length
112 if (chunkCount > 1) debugLog(`Chunked staged files into ${chunkCount} part`, chunkCount)
113
114 // lint-staged 10 will automatically add modifications to index
115 // Warn user when their command includes `git add`
116 let hasDeprecatedGitAdd = false
117
118 const listrOptions = {
119 ctx,
120 exitOnError: false,
121 nonTTYRenderer: 'verbose',
122 registerSignalListeners: false,
123 ...getRenderer({ debug, quiet }),
124 }
125
126 const listrTasks = []
127
128 // Set of all staged files that matched a task glob. Values in a set are unique.
129 const matchedFiles = new Set()
130
131 for (const [index, files] of stagedFileChunks.entries()) {
132 const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative })
133 const chunkListrTasks = []
134
135 for (const task of chunkTasks) {
136 const subTasks = await makeCmdTasks({
137 commands: task.commands,
138 files: task.fileList,
139 gitDir,
140 renderer: listrOptions.renderer,
141 shell,
142 verbose,
143 })
144
145 // Add files from task to match set
146 task.fileList.forEach((file) => {
147 matchedFiles.add(file)
148 })
149
150 hasDeprecatedGitAdd =
151 hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
152
153 chunkListrTasks.push({
154 title: `Running tasks for ${task.pattern}`,
155 task: async () =>
156 new Listr(subTasks, {
157 // In sub-tasks we don't want to run concurrently
158 // and we want to abort on errors
159 ...listrOptions,
160 concurrent: false,
161 exitOnError: true,
162 }),
163 skip: () => {
164 // Skip task when no files matched
165 if (task.fileList.length === 0) {
166 return `No staged files match ${task.pattern}`
167 }
168 return false
169 },
170 })
171 }
172
173 listrTasks.push({
174 // No need to show number of task chunks when there's only one
175 title:
176 chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...',
177 task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }),
178 skip: () => {
179 // Skip if the first step (backup) failed
180 if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
181 // Skip chunk when no every task is skipped (due to no matches)
182 if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.'
183 return false
184 },
185 })
186 }
187
188 if (hasDeprecatedGitAdd) {
189 logger.warn(DEPRECATED_GIT_ADD)
190 }
191
192 // If all of the configured tasks should be skipped
193 // avoid executing any lint-staged logic
194 if (listrTasks.every((task) => task.skip())) {
195 if (!quiet) ctx.output.push(NO_TASKS)
196 return ctx
197 }
198
199 // Chunk matched files for better Windows compatibility
200 const matchedFileChunks = chunkFiles({
201 // matched files are relative to `cwd`, not `gitDir`, when `relative` is used
202 baseDir: cwd,
203 files: Array.from(matchedFiles),
204 maxArgLength,
205 relative: false,
206 })
207
208 const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks })
209
210 const runner = new Listr(
211 [
212 {
213 title: 'Preparing...',
214 task: (ctx) => git.prepare(ctx),
215 },
216 {
217 title: 'Hiding unstaged changes to partially staged files...',
218 task: (ctx) => git.hideUnstagedChanges(ctx),
219 enabled: hasPartiallyStagedFiles,
220 },
221 ...listrTasks,
222 {
223 title: 'Applying modifications...',
224 task: (ctx) => git.applyModifications(ctx),
225 skip: applyModificationsSkipped,
226 },
227 {
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...',
235 task: (ctx) => git.restoreOriginalState(ctx),
236 enabled: restoreOriginalStateEnabled,
237 skip: restoreOriginalStateSkipped,
238 },
239 {
240 title: 'Cleaning up...',
241 task: (ctx) => git.cleanup(ctx),
242 enabled: cleanupEnabled,
243 skip: cleanupSkipped,
244 },
245 ],
246 listrOptions
247 )
248
249 await runner.run()
250
251 if (ctx.errors.size > 0) {
252 throw createError(ctx)
253 }
254
255 return ctx
256}