88864cf3b83fca3d06c37f97206a64914681b92ae6ba796b33e60f3179ff7fe3c2f4d4b3f003994f1bd23a785b51546902bb3ce5b1c92a4a8bc64aa74939b5 7.8 KB

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