UNPKG

7.97 kBJavaScriptView Raw
1'use strict'
2
3/** @typedef {import('./index').Logger} Logger */
4
5const { Listr } = require('listr2')
6
7const chunkFiles = require('./chunkFiles')
8const debugLog = require('debug')('lint-staged:run')
9const execGit = require('./execGit')
10const generateTasks = require('./generateTasks')
11const getRenderer = require('./getRenderer')
12const getStagedFiles = require('./getStagedFiles')
13const GitWorkflow = require('./gitWorkflow')
14const makeCmdTasks = require('./makeCmdTasks')
15const {
16 DEPRECATED_GIT_ADD,
17 FAILED_GET_STAGED_FILES,
18 NOT_GIT_REPO,
19 NO_STAGED_FILES,
20 NO_TASKS,
21 SKIPPED_GIT_ERROR,
22 skippingBackup,
23} = require('./messages')
24const resolveGitRepo = require('./resolveGitRepo')
25const {
26 applyModificationsSkipped,
27 cleanupEnabled,
28 cleanupSkipped,
29 getInitialState,
30 hasPartiallyStagedFiles,
31 restoreOriginalStateEnabled,
32 restoreOriginalStateSkipped,
33 restoreUnstagedChangesSkipped,
34} = require('./state')
35const { GitRepoError, GetStagedFilesError, GitError } = require('./symbols')
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 */
57const 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 ...getRenderer({ debug, quiet }),
123 }
124
125 const listrTasks = []
126
127 // Set of all staged files that matched a task glob. Values in a set are unique.
128 const matchedFiles = new Set()
129
130 for (const [index, files] of stagedFileChunks.entries()) {
131 const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative })
132 const chunkListrTasks = []
133
134 for (const task of chunkTasks) {
135 const subTasks = await makeCmdTasks({
136 commands: task.commands,
137 files: task.fileList,
138 gitDir,
139 renderer: listrOptions.renderer,
140 shell,
141 verbose,
142 })
143
144 // Add files from task to match set
145 task.fileList.forEach((file) => {
146 matchedFiles.add(file)
147 })
148
149 hasDeprecatedGitAdd = subTasks.some((subTask) => subTask.command === 'git add')
150
151 chunkListrTasks.push({
152 title: `Running tasks for ${task.pattern}`,
153 task: async () =>
154 new Listr(subTasks, {
155 // In sub-tasks we don't want to run concurrently
156 // and we want to abort on errors
157 ...listrOptions,
158 concurrent: false,
159 exitOnError: true,
160 }),
161 skip: () => {
162 // Skip task when no files matched
163 if (task.fileList.length === 0) {
164 return `No staged files match ${task.pattern}`
165 }
166 return false
167 },
168 })
169 }
170
171 listrTasks.push({
172 // No need to show number of task chunks when there's only one
173 title:
174 chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...',
175 task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }),
176 skip: () => {
177 // Skip if the first step (backup) failed
178 if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
179 // Skip chunk when no every task is skipped (due to no matches)
180 if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.'
181 return false
182 },
183 })
184 }
185
186 if (hasDeprecatedGitAdd) {
187 logger.warn(DEPRECATED_GIT_ADD)
188 }
189
190 // If all of the configured tasks should be skipped
191 // avoid executing any lint-staged logic
192 if (listrTasks.every((task) => task.skip())) {
193 if (!quiet) ctx.output.push(NO_TASKS)
194 return ctx
195 }
196
197 // Chunk matched files for better Windows compatibility
198 const matchedFileChunks = chunkFiles({
199 // matched files are relative to `cwd`, not `gitDir`, when `relative` is used
200 baseDir: cwd,
201 files: Array.from(matchedFiles),
202 maxArgLength,
203 relative: false,
204 })
205
206 const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks })
207
208 const runner = new Listr(
209 [
210 {
211 title: 'Preparing...',
212 task: (ctx) => git.prepare(ctx),
213 },
214 {
215 title: 'Hiding unstaged changes to partially staged files...',
216 task: (ctx) => git.hideUnstagedChanges(ctx),
217 enabled: hasPartiallyStagedFiles,
218 },
219 ...listrTasks,
220 {
221 title: 'Applying modifications...',
222 task: (ctx) => git.applyModifications(ctx),
223 skip: applyModificationsSkipped,
224 },
225 {
226 title: 'Restoring unstaged changes to partially staged files...',
227 task: (ctx) => git.restoreUnstagedChanges(ctx),
228 enabled: hasPartiallyStagedFiles,
229 skip: restoreUnstagedChangesSkipped,
230 },
231 {
232 title: 'Reverting to original state because of errors...',
233 task: (ctx) => git.restoreOriginalState(ctx),
234 enabled: restoreOriginalStateEnabled,
235 skip: restoreOriginalStateSkipped,
236 },
237 {
238 title: 'Cleaning up...',
239 task: (ctx) => git.cleanup(ctx),
240 enabled: cleanupEnabled,
241 skip: cleanupSkipped,
242 },
243 ],
244 listrOptions
245 )
246
247 await runner.run()
248
249 if (ctx.errors.size > 0) {
250 throw createError(ctx)
251 }
252
253 return ctx
254}
255
256module.exports = runAll