UNPKG

7.97 kBJavaScriptView Raw
1'use strict'
2
3/** @typedef {import('./index').Logger} Logger */
4
5const chalk = require('chalk')
6const Listr = require('listr')
7const symbols = require('log-symbols')
8
9const chunkFiles = require('./chunkFiles')
10const generateTasks = require('./generateTasks')
11const getStagedFiles = require('./getStagedFiles')
12const GitWorkflow = require('./gitWorkflow')
13const makeCmdTasks = require('./makeCmdTasks')
14const resolveGitRepo = require('./resolveGitRepo')
15
16const debugLog = require('debug')('lint-staged:run')
17
18const getRenderer = ({ debug, quiet }) => {
19 if (quiet) return 'silent'
20 // Better support for dumb terminals: https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals
21 const isDumbTerminal = process.env.TERM === 'dumb'
22 if (debug || isDumbTerminal) return 'verbose'
23 return 'update'
24}
25
26const MESSAGES = {
27 TASK_ERROR: 'Skipped because of errors from tasks.',
28 GIT_ERROR: 'Skipped because of previous git error.'
29}
30
31const shouldSkipApplyModifications = ctx => {
32 // Should be skipped in case of git errors
33 if (ctx.gitError) {
34 return MESSAGES.GIT_ERROR
35 }
36 // Should be skipped when tasks fail
37 if (ctx.taskError) {
38 return MESSAGES.TASK_ERROR
39 }
40}
41
42const shouldSkipRevert = ctx => {
43 // Should be skipped in case of unknown git errors
44 if (ctx.gitError && !ctx.gitApplyEmptyCommit && !ctx.gitApplyModificationsError) {
45 return MESSAGES.GIT_ERROR
46 }
47}
48
49const shouldSkipCleanup = ctx => {
50 // Should be skipped in case of unknown git errors
51 if (ctx.gitError && !ctx.gitApplyEmptyCommit && !ctx.gitApplyModificationsError) {
52 return MESSAGES.GIT_ERROR
53 }
54 // Should be skipped when reverting to original state fails
55 if (ctx.gitRestoreOriginalStateError) {
56 return MESSAGES.GIT_ERROR
57 }
58}
59
60/**
61 * Executes all tasks and either resolves or rejects the promise
62 *
63 * @param {object} options
64 * @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
65 * @param {Object} [options.config] - Task configuration
66 * @param {Object} [options.cwd] - Current working directory
67 * @param {number} [options.maxArgLength] - Maximum argument string length
68 * @param {boolean} [options.relative] - Pass relative filepaths to tasks
69 * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
70 * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
71 * @param {boolean} [options.debug] - Enable debug mode
72 * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
73 * @param {Logger} logger
74 * @returns {Promise}
75 */
76const runAll = async (
77 {
78 allowEmpty = false,
79 config,
80 cwd = process.cwd(),
81 debug = false,
82 maxArgLength,
83 quiet = false,
84 relative = false,
85 shell = false,
86 concurrent = true
87 },
88 logger = console
89) => {
90 debugLog('Running all linter scripts')
91
92 const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
93 if (!gitDir) throw new Error('Current directory is not a git directory!')
94
95 const files = await getStagedFiles({ cwd: gitDir })
96 if (!files) throw new Error('Unable to get staged files!')
97 debugLog('Loaded list of staged files in git:\n%O', files)
98
99 // If there are no files avoid executing any lint-staged logic
100 if (files.length === 0) {
101 logger.log('No staged files found.')
102 return 'No tasks to run.'
103 }
104
105 const stagedFileChunks = chunkFiles({ files, gitDir, maxArgLength, relative })
106 const chunkCount = stagedFileChunks.length
107 if (chunkCount > 1) debugLog(`Chunked staged files into ${chunkCount} part`, chunkCount)
108
109 // lint-staged 10 will automatically add modifications to index
110 // Warn user when their command includes `git add`
111 let hasDeprecatedGitAdd = false
112
113 const listrOptions = {
114 dateFormat: false,
115 exitOnError: false,
116 renderer: getRenderer({ debug, quiet })
117 }
118
119 const listrTasks = []
120
121 for (const [index, files] of stagedFileChunks.entries()) {
122 const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative })
123 const chunkListrTasks = []
124
125 for (const task of chunkTasks) {
126 const subTasks = await makeCmdTasks({
127 commands: task.commands,
128 files: task.fileList,
129 gitDir,
130 shell
131 })
132
133 if (subTasks.some(subTask => subTask.command === 'git add')) {
134 hasDeprecatedGitAdd = true
135 }
136
137 chunkListrTasks.push({
138 title: `Running tasks for ${task.pattern}`,
139 task: async () =>
140 new Listr(subTasks, {
141 // In sub-tasks we don't want to run concurrently
142 // and we want to abort on errors
143 dateFormat: false,
144 concurrent: false,
145 exitOnError: true
146 }),
147 skip: () => {
148 // Skip task when no files matched
149 if (task.fileList.length === 0) {
150 return `No staged files match ${task.pattern}`
151 }
152 return false
153 }
154 })
155 }
156
157 listrTasks.push({
158 // No need to show number of task chunks when there's only one
159 title:
160 chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...',
161 task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }),
162 skip: (ctx = {}) => {
163 // Skip if the first step (backup) failed
164 if (ctx.gitError) return MESSAGES.GIT_ERROR
165 // Skip chunk when no every task is skipped (due to no matches)
166 if (chunkListrTasks.every(task => task.skip())) return 'No tasks to run.'
167 return false
168 }
169 })
170 }
171
172 if (hasDeprecatedGitAdd) {
173 logger.warn(`${symbols.warning} ${chalk.yellow(
174 `Some of your tasks use \`git add\` command. Please remove it from the config since all modifications made by tasks will be automatically added to the git commit index.`
175 )}
176`)
177 }
178
179 // If all of the configured tasks should be skipped
180 // avoid executing any lint-staged logic
181 if (listrTasks.every(task => task.skip())) {
182 logger.log('No staged files match any of provided globs.')
183 return 'No tasks to run.'
184 }
185
186 const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, stagedFileChunks })
187
188 const runner = new Listr(
189 [
190 {
191 title: 'Preparing...',
192 task: ctx => git.stashBackup(ctx)
193 },
194 ...listrTasks,
195 {
196 title: 'Applying modifications...',
197 task: ctx => git.applyModifications(ctx),
198 skip: shouldSkipApplyModifications
199 },
200 {
201 title: 'Reverting to original state...',
202 task: ctx => git.restoreOriginalState(ctx),
203 enabled: ctx => ctx.taskError || ctx.gitApplyEmptyCommit || ctx.gitApplyModificationsError,
204 skip: shouldSkipRevert
205 },
206 {
207 title: 'Cleaning up...',
208 task: ctx => git.dropBackup(ctx),
209 skip: shouldSkipCleanup
210 }
211 ],
212 listrOptions
213 )
214
215 try {
216 await runner.run({})
217 } catch (error) {
218 if (error.context.gitApplyEmptyCommit) {
219 logger.warn(`
220 ${symbols.warning} ${chalk.yellow(`lint-staged prevented an empty git commit.
221 Use the --allow-empty option to continue, or check your task configuration`)}
222`)
223 } else if (error.context.gitError && !error.context.gitGetBackupStashError) {
224 logger.error(`\n ${symbols.error} ${chalk.red(`lint-staged failed due to a git error.`)}`)
225
226 if (error.context.emptyGitRepo) {
227 logger.error(
228 `\n The initial commit is needed for lint-staged to work.
229 Please use the --no-verify flag to skip running lint-staged.`
230 )
231 } else {
232 // No sense to show this if the backup stash itself is missing.
233 logger.error(` Any lost modifications can be restored from a git stash:
234
235 > git stash list
236 stash@{0}: On master: automatic lint-staged backup
237 > git stash pop stash@{0}\n`)
238 }
239 }
240
241 throw error
242 }
243}
244
245module.exports = runAll
246
247module.exports.shouldSkip = {
248 shouldSkipApplyModifications,
249 shouldSkipRevert,
250 shouldSkipCleanup
251}