Repository: okonet/lint-staged
Pull request #743 (2019-11-23 01:07:27)

Add support for concurrent CLI option
Supports <boolean | number> (true/false, max number of tasks to run concurrently).

I don't love the tests, but I'm open to suggestions on how to improve them.

jeffreyffs: @iiroj, I re-made the MR as I realized there were a bunch of errors the first time around. Should have addressed all your feedback and added some tests. Let me know what you think!
codecov[bot]: # [Codecov](https://codecov.io/gh/okonet/lint-staged/pull/743?src=pr&el=h1) Report
> Merging [#743](https://codecov.io/gh/okonet/lint-staged/pull/743?src=pr&el=desc) into [master](https://codecov.io/gh/okonet/lint-staged/commit/99c317a43ef96aa7f1c929b436086d1b110b05f9?src=pr&el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/okonet/lint-staged/pull/743/graphs/tree.svg?width=650&token=LSoEgsjk4O&height=150&src=pr)](https://codecov.io/gh/okonet/lint-staged/pull/743?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #743      +/-   ##
==========================================
+ Coverage   99.67%   99.68%   +0.01%     
==========================================
  Files          11       11              
  Lines         309      319      +10     
  Branches       64       74      +10     
==========================================
+ Hits          308      318      +10     
  Misses          1        1
```


| [Impacted Files](https://codecov.io/gh/okonet/lint-staged/pull/743?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/index.js](https://codecov.io/gh/okonet/lint-staged/pull/743/diff?src=pr&el=tree#diff-c3JjL2luZGV4Lmpz) | `100% <100%> (ø)` | :arrow_up: |
| [src/runAll.js](https://codecov.io/gh/okonet/lint-staged/pull/743/diff?src=pr&el=tree#diff-c3JjL3J1bkFsbC5qcw==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/okonet/lint-staged/pull/743?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/okonet/lint-staged/pull/743?src=pr&el=footer). Last update [99c317a...15e2c47](https://codecov.io/gh/okonet/lint-staged/pull/743?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

jeffreyffs: Updated the README

Patch:
diff --git a/README.md b/README.md
index b883bae..786fbf3 100644
--- a/README.md
+++ b/README.md
@@ -70,13 +70,14 @@ $ npx lint-staged --help
 Usage: lint-staged [options]
 
 Options:
-  -V, --version        output the version number
-  -c, --config [path]  Path to configuration file
-  -r, --relative       Pass relative filepaths to tasks
-  -x, --shell          Skip parsing of tasks for better shell support
-  -q, --quiet          Disable lint-staged’s own console output
-  -d, --debug          Enable debug mode
-  -h, --help           output usage information
+  -V, --version                     output the version number
+  -c, --config [path]               Path to configuration file
+  -r, --relative                    Pass relative filepaths to tasks
+  -x, --shell                       Skip parsing of tasks for better shell support
+  -q, --quiet                       Disable lint-staged’s own console output
+  -d, --debug                       Enable debug mode
+  -p, --concurrent [parallel tasks] The number of tasks to run concurrently, or false to run tasks sequentially
+  -h, --help                        output usage information
 ```
 
 - **`--config [path]`**: This can be used to manually specify the `lint-staged` config file location. However, if the specified file cannot be found, it will error out instead of performing the usual search. You may pass a npm package name for configuration also.
@@ -86,6 +87,10 @@ Options:
 - **`--debug`**: Enabling the debug mode does the following:
   - `lint-staged` uses the [debug](https://github.com/visionmedia/debug) module internally to log information about staged files, commands being executed, location of binaries, etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`.
   - Use the [`verbose` renderer](https://github.com/SamVerschueren/listr-verbose-renderer) for `listr`.
+- **`--concurrent [number | (true/false)]`**: Controls the concurrency of tasks being run by lint-staged. **NOTE**: This does NOT affect the concurrency of subtasks (they will always be run sequentially). Possible values are:
+  - `false`: Run all tasks serially
+  - `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible.
+  - `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`.
 
 ## Configuration
 
diff --git a/bin/lint-staged b/bin/lint-staged
index 2f26eee..7bb8858 100755
--- a/bin/lint-staged
+++ b/bin/lint-staged
@@ -34,6 +34,11 @@ cmdline
   .option('-x, --shell', 'Skip parsing of tasks for better shell support')
   .option('-q, --quiet', 'Disable lint-staged’s own console output')
   .option('-d, --debug', 'Enable debug mode')
+  .option(
+    '-p, --concurrent <parallel tasks>',
+    'The number of tasks to run concurrently, or false to run tasks serially',
+    true
+  )
   .parse(process.argv)
 
 if (cmdline.debug) {
@@ -47,7 +52,8 @@ lintStaged({
   relative: !!cmdline.relative,
   shell: !!cmdline.shell,
   quiet: !!cmdline.quiet,
-  debug: !!cmdline.debug
+  debug: !!cmdline.debug,
+  concurrent: cmdline.concurrent
 })
   .then(passed => {
     process.exitCode = passed ? 0 : 1
diff --git a/src/index.js b/src/index.js
index 15334dc..eb83af5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -48,12 +48,21 @@ function loadConfig(configPath) {
  * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
  * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
  * @param {boolean} [options.debug] - Enable debug mode
+ * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
  * @param {Logger} [logger]
  *
  * @returns {Promise<boolean>} Promise of whether the linting passed or failed
  */
 module.exports = function lintStaged(
-  { configPath, config, relative = false, shell = false, quiet = false, debug = false } = {},
+  {
+    configPath,
+    config,
+    relative = false,
+    shell = false,
+    quiet = false,
+    debug = false,
+    concurrent = true
+  } = {},
   logger = console
 ) {
   debugLog('Loading config using `cosmiconfig`')
@@ -76,7 +85,7 @@ module.exports = function lintStaged(
         debugLog('lint-staged config:\n%O', config)
       }
 
-      return runAll({ config, relative, shell, quiet, debug }, logger)
+      return runAll({ config, relative, shell, quiet, debug, concurrent }, logger)
         .then(() => {
           debugLog('tasks were executed successfully!')
           return Promise.resolve(true)
diff --git a/src/runAll.js b/src/runAll.js
index 5f4d652..3e6607b 100644
--- a/src/runAll.js
+++ b/src/runAll.js
@@ -33,15 +33,23 @@ const MAX_ARG_LENGTH =
  * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
  * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
  * @param {boolean} [options.debug] - Enable debug mode
+ * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
  * @param {Logger} logger
  * @returns {Promise}
  */
 module.exports = async function runAll(
-  { config, cwd = process.cwd(), debug = false, quiet = false, relative = false, shell = false },
+  {
+    config,
+    cwd = process.cwd(),
+    debug = false,
+    quiet = false,
+    relative = false,
+    shell = false,
+    concurrent = true
+  },
   logger = console
 ) {
   debugLog('Running all linter scripts')
-
   const gitDir = await resolveGitDir({ cwd })
 
   if (!gitDir) {
@@ -120,7 +128,7 @@ https://github.com/okonet/lint-staged#using-js-functions-to-customize-linter-com
       },
       {
         title: 'Running tasks...',
-        task: () => new Listr(tasks, { ...listrOptions, concurrent: true, exitOnError: false })
+        task: () => new Listr(tasks, { ...listrOptions, concurrent, exitOnError: false })
       },
       {
         title: 'Updating stash...',


Test patch:
diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js
index cacbd7d..baafe8c 100644
--- a/test/runAll.unmocked.spec.js
+++ b/test/runAll.unmocked.spec.js
@@ -89,6 +89,81 @@ describe('runAll', () => {
     expect(await readFile('test.js')).toEqual(testJsFilePretty)
   })
 
+  it('Should succeed when conflicting tasks sequentially edit a file', async () => {
+    await appendFile('test.js', testJsFileUgly)
+
+    await fs.mkdir(cwd + '/files')
+    await appendFile('file.js', testJsFileUgly, cwd + '/files')
+
+    await execGit(['add', 'test.js'])
+    await execGit(['add', 'files'])
+
+    const success = await gitCommit({
+      config: {
+        'file.js': ['prettier --write', 'git add'],
+        'test.js': files => {
+          // concurrent: false, means this should still work
+          fs.removeSync(`${cwd}/files`)
+          return [`prettier --write ${files.join(' ')}`, `git add ${files.join(' ')}`]
+        }
+      },
+      concurrent: false
+    })
+
+    expect(success).toEqual(true)
+  })
+
+  it('Should fail when conflicting tasks concurrently edit a file', async () => {
+    await appendFile('test.js', testJsFileUgly)
+    await appendFile('test2.js', testJsFileUgly)
+
+    await fs.mkdir(cwd + '/files')
+    await appendFile('file.js', testJsFileUgly, cwd + '/files')
+
+    await execGit(['add', 'test.js'])
+    await execGit(['add', 'test2.js'])
+    await execGit(['add', 'files'])
+
+    const success = await gitCommit({
+      config: {
+        'file.js': ['prettier --write', 'git add'],
+        'test.js': ['prettier --write', 'git add'],
+        'test2.js': files => {
+          // remove `files` so the 1st command should fail
+          fs.removeSync(`${cwd}/files`)
+          return [`prettier --write ${files.join(' ')}`, `git add ${files.join(' ')}`]
+        }
+      },
+      concurrent: true
+    })
+
+    expect(success).toEqual(false)
+  })
+
+  it('Should succeed when conflicting tasks concurrently (max concurrency 1) edit a file', async () => {
+    await appendFile('test.js', testJsFileUgly)
+
+    await fs.mkdir(cwd + '/files')
+    await appendFile('file.js', testJsFileUgly, cwd + '/files')
+
+    await execGit(['add', 'test.js'])
+    await execGit(['add', 'files'])
+
+    const success = await gitCommit({
+      config: {
+        'file.js': ['prettier --write', 'git add'],
+        'test2.js': files => {
+          // concurrency of one should prevent save this operation
+          fs.removeSync(`${cwd}/files`)
+          return [`prettier --write ${files.join(' ')}`, `git add ${files.join(' ')}`]
+        }
+      },
+      concurrent: 1
+    })
+
+    expect(success).toEqual(true)
+  })
+
   it('Should commit entire staged file when no errors and linter modifies file', async () => {
     // Stage ugly file
     await appendFile('test.js', testJsFileUgly)