'use strict' /** @typedef {import('./index').Logger} Logger */ const { Listr } = require('listr2') const chunkFiles = require('./chunkFiles') const debugLog = require('debug')('lint-staged:run') const execGit = require('./execGit') const generateTasks = require('./generateTasks') const getRenderer = require('./getRenderer') const getStagedFiles = require('./getStagedFiles') const GitWorkflow = require('./gitWorkflow') const makeCmdTasks = require('./makeCmdTasks') const { DEPRECATED_GIT_ADD, FAILED_GET_STAGED_FILES, NOT_GIT_REPO, NO_STAGED_FILES, NO_TASKS, SKIPPED_GIT_ERROR, skippingBackup, } = require('./messages') const resolveGitRepo = require('./resolveGitRepo') const { applyModificationsSkipped, cleanupEnabled, cleanupSkipped, getInitialState, hasPartiallyStagedFiles, restoreOriginalStateEnabled, restoreOriginalStateSkipped, restoreUnstagedChangesSkipped, } = require('./state') const { GitRepoError, GetStagedFilesError, GitError } = require('./symbols') const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ctx }) /** * Executes all tasks and either resolves or rejects the promise * * @param {object} options * @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially * @param {Object} [options.config] - Task configuration * @param {Object} [options.cwd] - Current working directory * @param {boolean} [options.debug] - Enable debug mode * @param {number} [options.maxArgLength] - Maximum argument string length * @param {boolean} [options.quiet] - Disable lint-staged’s own console output * @param {boolean} [options.relative] - Pass relative filepaths to tasks * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown * @param {Logger} logger * @returns {Promise} */ const runAll = async ( { allowEmpty = false, concurrent = true, config, cwd = process.cwd(), debug = false, maxArgLength, quiet = false, relative = false, shell = false, stash = true, verbose = false, }, logger = console ) => { debugLog('Running all linter scripts') const ctx = getInitialState({ quiet }) const { gitDir, gitConfigDir } = await resolveGitRepo(cwd) if (!gitDir) { if (!quiet) ctx.output.push(NOT_GIT_REPO) ctx.errors.add(GitRepoError) throw createError(ctx) } // Test whether we have any commits or not. // Stashing must be disabled with no initial commit. const hasInitialCommit = await execGit(['log', '-1'], { cwd: gitDir }) .then(() => true) .catch(() => false) // Lint-staged should create a backup stash only when there's an initial commit ctx.shouldBackup = hasInitialCommit && stash if (!ctx.shouldBackup) { logger.warn(skippingBackup(hasInitialCommit)) } const files = await getStagedFiles({ cwd: gitDir }) if (!files) { if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES) ctx.errors.add(GetStagedFilesError) throw createError(ctx, GetStagedFilesError) } debugLog('Loaded list of staged files in git:\n%O', files) // If there are no files avoid executing any lint-staged logic if (files.length === 0) { if (!quiet) ctx.output.push(NO_STAGED_FILES) return ctx } const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative }) const chunkCount = stagedFileChunks.length if (chunkCount > 1) debugLog(`Chunked staged files into ${chunkCount} part`, chunkCount) // lint-staged 10 will automatically add modifications to index // Warn user when their command includes `git add` let hasDeprecatedGitAdd = false const listrOptions = { ctx, exitOnError: false, nonTTYRenderer: 'verbose', registerSignalListeners: false, ...getRenderer({ debug, quiet }), } const listrTasks = [] // Set of all staged files that matched a task glob. Values in a set are unique. const matchedFiles = new Set() for (const [index, files] of stagedFileChunks.entries()) { const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative }) const chunkListrTasks = [] for (const task of chunkTasks) { const subTasks = await makeCmdTasks({ commands: task.commands, files: task.fileList, gitDir, renderer: listrOptions.renderer, shell, verbose, }) // Add files from task to match set task.fileList.forEach((file) => { matchedFiles.add(file) }) hasDeprecatedGitAdd = subTasks.some((subTask) => subTask.command === 'git add') chunkListrTasks.push({ title: `Running tasks for ${task.pattern}`, task: async () => new Listr(subTasks, { // In sub-tasks we don't want to run concurrently // and we want to abort on errors ...listrOptions, concurrent: false, exitOnError: true, }), skip: () => { // Skip task when no files matched if (task.fileList.length === 0) { return `No staged files match ${task.pattern}` } return false }, }) } listrTasks.push({ // No need to show number of task chunks when there's only one title: chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...', task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }), skip: () => { // Skip if the first step (backup) failed if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR // Skip chunk when no every task is skipped (due to no matches) if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.' return false }, }) } if (hasDeprecatedGitAdd) { logger.warn(DEPRECATED_GIT_ADD) } // If all of the configured tasks should be skipped // avoid executing any lint-staged logic if (listrTasks.every((task) => task.skip())) { if (!quiet) ctx.output.push(NO_TASKS) return ctx } // Chunk matched files for better Windows compatibility const matchedFileChunks = chunkFiles({ // matched files are relative to `cwd`, not `gitDir`, when `relative` is used baseDir: cwd, files: Array.from(matchedFiles), maxArgLength, relative: false, }) const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks }) const runner = new Listr( [ { title: 'Preparing...', task: (ctx) => git.prepare(ctx), }, { title: 'Hiding unstaged changes to partially staged files...', task: (ctx) => git.hideUnstagedChanges(ctx), enabled: hasPartiallyStagedFiles, }, ...listrTasks, { title: 'Applying modifications...', task: (ctx) => git.applyModifications(ctx), skip: applyModificationsSkipped, }, { title: 'Restoring unstaged changes to partially staged files...', task: (ctx) => git.restoreUnstagedChanges(ctx), enabled: hasPartiallyStagedFiles, skip: restoreUnstagedChangesSkipped, }, { title: 'Reverting to original state because of errors...', task: (ctx) => git.restoreOriginalState(ctx), enabled: restoreOriginalStateEnabled, skip: restoreOriginalStateSkipped, }, { title: 'Cleaning up...', task: (ctx) => git.cleanup(ctx), enabled: cleanupEnabled, skip: cleanupSkipped, }, ], listrOptions ) await runner.run() if (ctx.errors.size > 0) { throw createError(ctx) } return ctx } module.exports = runAll