UNPKG

8.01 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 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 = subTasks.some((subTask) => subTask.command === 'git add')
151
152 chunkListrTasks.push({
153 title: `Running tasks for ${task.pattern}`,
154 task: async () =>
155 new Listr(subTasks, {
156 // In sub-tasks we don't want to run concurrently
157 // and we want to abort on errors
158 ...listrOptions,
159 concurrent: false,
160 exitOnError: true,
161 }),
162 skip: () => {
163 // Skip task when no files matched
164 if (task.fileList.length === 0) {
165 return `No staged files match ${task.pattern}`
166 }
167 return false
168 },
169 })
170 }
171
172 listrTasks.push({
173 // No need to show number of task chunks when there's only one
174 title:
175 chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...',
176 task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }),
177 skip: () => {
178 // Skip if the first step (backup) failed
179 if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR
180 // Skip chunk when no every task is skipped (due to no matches)
181 if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.'
182 return false
183 },
184 })
185 }
186
187 if (hasDeprecatedGitAdd) {
188 logger.warn(DEPRECATED_GIT_ADD)
189 }
190
191 // If all of the configured tasks should be skipped
192 // avoid executing any lint-staged logic
193 if (listrTasks.every((task) => task.skip())) {
194 if (!quiet) ctx.output.push(NO_TASKS)
195 return ctx
196 }
197
198 // Chunk matched files for better Windows compatibility
199 const matchedFileChunks = chunkFiles({
200 // matched files are relative to `cwd`, not `gitDir`, when `relative` is used
201 baseDir: cwd,
202 files: Array.from(matchedFiles),
203 maxArgLength,
204 relative: false,
205 })
206
207 const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks })
208
209 const runner = new Listr(
210 [
211 {
212 title: 'Preparing...',
213 task: (ctx) => git.prepare(ctx),
214 },
215 {
216 title: 'Hiding unstaged changes to partially staged files...',
217 task: (ctx) => git.hideUnstagedChanges(ctx),
218 enabled: hasPartiallyStagedFiles,
219 },
220 ...listrTasks,
221 {
222 title: 'Applying modifications...',
223 task: (ctx) => git.applyModifications(ctx),
224 skip: applyModificationsSkipped,
225 },
226 {
227 title: 'Restoring unstaged changes to partially staged files...',
228 task: (ctx) => git.restoreUnstagedChanges(ctx),
229 enabled: hasPartiallyStagedFiles,
230 skip: restoreUnstagedChangesSkipped,
231 },
232 {
233 title: 'Reverting to original state because of errors...',
234 task: (ctx) => git.restoreOriginalState(ctx),
235 enabled: restoreOriginalStateEnabled,
236 skip: restoreOriginalStateSkipped,
237 },
238 {
239 title: 'Cleaning up...',
240 task: (ctx) => git.cleanup(ctx),
241 enabled: cleanupEnabled,
242 skip: cleanupSkipped,
243 },
244 ],
245 listrOptions
246 )
247
248 await runner.run()
249
250 if (ctx.errors.size > 0) {
251 throw createError(ctx)
252 }
253
254 return ctx
255}
256
257module.exports = runAll