code:
# 🚫💩 lint-staged

[![npm version](https://badge.fury.io/js/lint-staged.svg)](https://badge.fury.io/js/lint-staged)

---

Run linters against staged git files and don't let :poop: slip into your code base!

```bash
npm install --save-dev lint-staged # requires further setup
```

```
$ git commit

✔ Preparing lint-staged...
❯ Running tasks for staged files...
  ❯ packages/frontend/.lintstagedrc.json — 1 file
    ↓ *.js — no files [SKIPPED]
    ❯ *.{json,md} — 1 file
      ⠹ prettier --write
  ↓ packages/backend/.lintstagedrc.json — 2 files
    ❯ *.js — 2 files
      ⠼ eslint --fix
    ↓ *.{json,md} — no files [SKIPPED]
◼ Applying modifications from tasks...
◼ Cleaning up temporary files...
```

<details>
<summary>See asciinema video</summary>

[![asciicast](https://asciinema.org/a/199934.svg)](https://asciinema.org/a/199934)

</details>

## Why

Linting makes more sense when run before committing your code. By doing so you can ensure no errors go into the repository and enforce code style. But running a lint process on a whole project is slow, and linting results can be irrelevant. Ultimately you only want to lint files that will be committed.

This project contains a script that will run arbitrary shell tasks with a list of staged files as an argument, filtered by a specified glob pattern.

## Related blog posts and talks

- [Introductory Medium post - Andrey Okonetchnikov, 2016](https://medium.com/@okonetchnikov/make-linting-great-again-f3890e1ad6b8#.8qepn2b5l)
- [Running Jest Tests Before Each Git Commit - Ben McCormick, 2017](https://benmccormick.org/2017/02/26/running-jest-tests-before-each-git-commit/)
- [AgentConf presentation - Andrey Okonetchnikov, 2018](https://www.youtube.com/watch?v=-mhY7e-EsC4)
- [SurviveJS interview - Juho Vepsäläinen and Andrey Okonetchnikov, 2018](https://survivejs.com/blog/lint-staged-interview/)
- [Prettier your CSharp with `dotnet-format` and `lint-staged`](https://johnnyreilly.com/2020/12/22/prettier-your-csharp-with-dotnet-format-and-lint-staged)

> If you've written one, please submit a PR with the link to it!

## Installation and setup

To install _lint-staged_ in the recommended way, you need to:

1. Install _lint-staged_ itself:
   - `npm install --save-dev lint-staged`
1. Set up the `pre-commit` git hook to run _lint-staged_
   - [Husky](https://github.com/typicode/husky) is a popular choice for configuring git hooks
   - Read more about git hooks [here](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)
1. Install some linters, like [ESLint](https://eslint.org) or [Prettier](https://prettier.io)
1. Configure _lint-staged_ to run linters and other tasks:
   - for example: `{ "*.js": "eslint" }` to run ESLint for all staged JS files
   - See [Configuration](#configuration) for more info

Don't forget to commit changes to `package.json` and `.husky` to share this setup with your team!

Now change a few files, `git add` or `git add --patch` some of them to your commit, and try to `git commit` them.

See [examples](#examples) and [configuration](#configuration) for more information.

## Changelog

See [Releases](https://github.com/okonet/lint-staged/releases).

### Migration

#### v15

- Since `v15.0.0` _lint-staged_ no longer supports Node.js 16. Please upgrade your Node.js version to at least `18.12.0`.

#### v14

- Since `v14.0.0` _lint-staged_ no longer supports Node.js 14. Please upgrade your Node.js version to at least `16.14.0`.

#### v13

- Since `v13.0.0` _lint-staged_ no longer supports Node.js 12. Please upgrade your Node.js version to at least `14.13.1`, or `16.0.0` onward.
- Version `v13.3.0` was incorrectly released including code of version `v14.0.0`. This means the breaking changes of `v14` are also included in `v13.3.0`, the last `v13` version released

#### v12

- Since `v12.0.0` _lint-staged_ is a pure ESM module, so make sure your Node.js version is at least `12.20.0`, `14.13.1`, or `16.0.0`. Read more about ESM modules from the official [Node.js Documentation site here](https://nodejs.org/api/esm.html#introduction).

#### v10

- From `v10.0.0` onwards any new modifications to originally staged files will be automatically added to the commit.
  If your task previously contained a `git add` step, please remove this.
  The automatic behaviour ensures there are less race-conditions,
  since trying to run multiple git operations at the same time usually results in an error.
- From `v10.0.0` onwards, lint-staged uses git stashes to improve speed and provide backups while running.
  Since git stashes require at least an initial commit, you shouldn't run lint-staged in an empty repo.
- From `v10.0.0` onwards, lint-staged requires Node.js version 10.13.0 or later.
- From `v10.0.0` onwards, lint-staged will abort the commit if linter tasks undo all staged changes. To allow creating an empty commit, please use the `--allow-empty` option.

## Command line flags

```
❯ npx lint-staged --help
Usage: lint-staged [options]

Options:
  -V, --version                      output the version number
  --allow-empty                      allow empty commits when tasks revert all staged changes (default: false)
  -p, --concurrent <number|boolean>  the number of tasks to run concurrently, or false for serial (default: true)
  -c, --config [path]                path to configuration file, or - to read from stdin
  --cwd [path]                       run all tasks in specific directory, instead of the current
  -d, --debug                        print additional debug information (default: false)
  --diff [string]                    override the default "--staged" flag of "git diff" to get list of files. Implies
                                     "--no-stash".
  --diff-filter [string]             override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
  --max-arg-length [number]          maximum length of the command-line argument string (default: 0)
  --no-stash                         disable the backup stash, and do not revert in case of errors. Implies
                                     "--no-hide-partially-staged".
  --no-hide-partially-staged         disable hiding unstaged changes from partially staged files
  -q, --quiet                        disable lint-staged’s own console output (default: false)
  -r, --relative                     pass relative filepaths to tasks (default: false)
  -x, --shell [path]                 skip parsing of tasks for better shell support (default: false)
  -v, --verbose                      show task output even when tasks succeed; by default only failed output is shown
                                     (default: false)
  -h, --help                         display help for command
```

- **`--allow-empty`**: By default, when linter tasks undo all staged changes, lint-staged will exit with an error and abort the commit. Use this flag to allow creating empty git commits.
- **`--concurrent [number|boolean]`**: Controls the [concurrency of tasks](#task-concurrency) 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`.
- **`--config [path]`**: Manually specify a path to a config file or npm package name. Note: when used, lint-staged won't perform the config file search and will print an error if the specified file cannot be found. If '-' is provided as the filename then the config will be read from stdin, allowing piping in the config like `cat my-config.json | npx lint-staged --config -`.
- **`--cwd [path]`**: By default tasks run in the current working directory. Use the `--cwd some/directory` to override this. The path can be absolute or relative to the current working directory.
- **`--debug`**: Run in debug mode. When set, it does the following:
  - uses [debug](https://github.com/visionmedia/debug) internally to log additional 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*`.
  - uses [`verbose` renderer](https://listr2.kilic.dev/renderers/verbose-renderer/) for `listr2`; this causes serial, uncoloured output to the terminal, instead of the default (beautified, dynamic) output.
    (the [`verbose` renderer](https://listr2.kilic.dev/renderers/verbose-renderer/) can also be activated by setting the `TERM=dumb` or `NODE_ENV=test` environment variables)
- **`--diff`**: By default linters are filtered against all files staged in git, generated from `git diff --staged`. This option allows you to override the `--staged` flag with arbitrary revisions. For example to get a list of changed files between two branches, use `--diff="branch1...branch2"`. You can also read more from about [git diff](https://git-scm.com/docs/git-diff) and [gitrevisions](https://git-scm.com/docs/gitrevisions). This option also implies `--no-stash`.
- **`--diff-filter`**: By default only files that are _added_, _copied_, _modified_, or _renamed_ are included. Use this flag to override the default `ACMR` value with something else: _added_ (`A`), _copied_ (`C`), _deleted_ (`D`), _modified_ (`M`), _renamed_ (`R`), _type changed_ (`T`), _unmerged_ (`U`), _unknown_ (`X`), or _pairing broken_ (`B`). See also the `git diff` docs for [--diff-filter](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203).
- **`--max-arg-length`**: long commands (a lot of files) are automatically split into multiple chunks when it detects the current shell cannot handle them. Use this flag to override the maximum length of the generated command string.
- **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit. Can be re-enabled with `--stash`. This option also implies `--no-hide-partially-staged`.
- **`--no-hide-partially-staged`**: By default, unstaged changes from partially staged files will be hidden. This option will disable this behavior and include all unstaged changes in partially staged files. Can be re-enabled with `--hide-partially-staged`
- **`--quiet`**: Supress all CLI output, except from tasks.
- **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
- **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option. To use a specific shell, use a path like `--shell "/bin/bash"`.
- **`--verbose`**: Show task output even when tasks succeed. By default only failed output is shown.

## Configuration

_Lint-staged_ can be configured in many ways:

- `lint-staged` object in your `package.json`, or [`package.yaml`](https://github.com/pnpm/pnpm/pull/1799)
- `.lintstagedrc` file in JSON or YML format, or you can be explicit with the file extension:
  - `.lintstagedrc.json`
  - `.lintstagedrc.yaml`
  - `.lintstagedrc.yml`
- `.lintstagedrc.mjs` or `lint-staged.config.mjs` file in ESM format
  - the default export value should be a configuration: `export default { ... }`
- `.lintstagedrc.cjs` or `lint-staged.config.cjs` file in CommonJS format
  - the exports value should be a configuration: `module.exports = { ... }`
- `lint-staged.config.js` or `.lintstagedrc.js` in either ESM or CommonJS format, depending on
  whether your project's _package.json_ contains the `"type": "module"` option or not.
- Pass a configuration file using the `--config` or `-c` flag

Configuration should be an object where each value is a command to run and its key is a glob pattern to use for this command. This package uses [micromatch](https://github.com/micromatch/micromatch) for glob patterns. JavaScript files can also export advanced configuration as a function. See [Using JS configuration files](#using-js-configuration-files) for more info.

You can also place multiple configuration files in different directories inside a project. For a given staged file, the closest configuration file will always be used. See ["How to use `lint-staged` in a multi-package monorepo?"](#how-to-use-lint-staged-in-a-multi-package-monorepo) for more info and an example.

#### `package.json` example:

```json
{
  "lint-staged": {
    "*": "your-cmd"
  }
}
```

#### `.lintstagedrc` example

```json
{
  "*": "your-cmd"
}
```

This config will execute `your-cmd` with the list of currently staged files passed as arguments.

So, considering you did `git add file1.ext file2.ext`, lint-staged will run the following command:

`your-cmd file1.ext file2.ext`

### Task concurrency

By default _lint-staged_ will run configured tasks concurrently. This means that for every glob, all the commands will be started at the same time. With the following config, both `eslint` and `prettier` will run at the same time:

```json
{
  "*.ts": "eslint",
  "*.md": "prettier --list-different"
}
```

This is typically not a problem since the globs do not overlap, and the commands do not make changes to the files, but only report possible errors (aborting the git commit). If you want to run multiple commands for the same set of files, you can use the array syntax to make sure commands are run in order. In the following example, `prettier` will run for both globs, and in addition `eslint` will run for `*.ts` files _after_ it. Both sets of commands (for each glob) are still started at the same time (but do not overlap).

```json
{
  "*.ts": ["prettier --list-different", "eslint"],
  "*.md": "prettier --list-different"
}
```

Pay extra attention when the configured globs overlap, and tasks make edits to files. For example, in this configuration `prettier` and `eslint` might try to make changes to the same `*.ts` file at the same time, causing a _race condition_:

```json
{
  "*": "prettier --write",
  "*.ts": "eslint --fix"
}
```

You can solve it using the negation pattern and the array syntax:

```json
{
  "!(*.ts)": "prettier --write",
  "*.ts": ["eslint --fix", "prettier --write"]
}
```

Another example in which tasks make edits to files and globs match multiple files but don't overlap:

```json
{
  "*.css": ["stylelint --fix", "prettier --write"],
  "*.{js,jsx}": ["eslint --fix", "prettier --write"],
  "!(*.css|*.js|*.jsx)": ["prettier --write"]
}
```

Or, if necessary, you can limit the concurrency using `--concurrent <number>` or disable it entirely with `--concurrent false`.

## Filtering files

Linter commands work on a subset of all staged files, defined by a _glob pattern_. lint-staged uses [micromatch](https://github.com/micromatch/micromatch) for matching files with the following rules:

- If the glob pattern contains no slashes (`/`), micromatch's `matchBase` option will be enabled, so globs match a file's basename regardless of directory:
  - `"*.js"` will match all JS files, like `/test.js` and `/foo/bar/test.js`
  - `"!(*test).js"` will match all JS files, except those ending in `test.js`, so `foo.js` but not `foo.test.js`
  - `"!(*.css|*.js)"` will match all files except CSS and JS files
- If the glob pattern does contain a slash (`/`), it will match for paths as well:
  - `"./*.js"` will match all JS files in the git repo root, so `/test.js` but not `/foo/bar/test.js`
  - `"foo/**/*.js"` will match all JS files inside the `/foo` directory, so `/foo/bar/test.js` but not `/test.js`

When matching, lint-staged will do the following

- Resolve the git root automatically, no configuration needed.
- Pick the staged files which are present inside the project directory.
- Filter them using the specified glob patterns.
- Pass absolute paths to the linters as arguments.

**NOTE:** `lint-staged` will pass _absolute_ paths to the linters to avoid any confusion in case they're executed in a different working directory (i.e. when your `.git` directory isn't the same as your `package.json` directory).

Also see [How to use `lint-staged` in a multi-package monorepo?](#how-to-use-lint-staged-in-a-multi-package-monorepo)

### Ignoring files

The concept of `lint-staged` is to run configured linter tasks (or other tasks) on files that are staged in git. `lint-staged` will always pass a list of all staged files to the task, and ignoring any files should be configured in the task itself.

Consider a project that uses [`prettier`](https://prettier.io/) to keep code format consistent across all files. The project also stores minified 3rd-party vendor libraries in the `vendor/` directory. To keep `prettier` from throwing errors on these files, the vendor directory should be added to prettier's ignore configuration, the `.prettierignore` file. Running `npx prettier .` will ignore the entire vendor directory, throwing no errors. When `lint-staged` is added to the project and configured to run prettier, all modified and staged files in the vendor directory will be ignored by prettier, even though it receives them as input.

In advanced scenarios, where it is impossible to configure the linter task itself to ignore files, but some staged files should still be ignored by `lint-staged`, it is possible to filter filepaths before passing them to tasks by using the function syntax. See [Example: Ignore files from match](#example-ignore-files-from-match).

## What commands are supported?

Supported are any executables installed locally or globally via `npm` as well as any executable from your \$PATH.

> Using globally installed scripts is discouraged, since lint-staged may not work for someone who doesn't have it installed.

`lint-staged` uses [execa](https://github.com/sindresorhus/execa#preferlocal) to locate locally installed scripts. So in your `.lintstagedrc` you can write:

```json
{
  "*.js": "eslint --fix"
}
```

This will result in _lint-staged_ running `eslint --fix file-1.js file-2.js`, when you have staged files `file-1.js`, `file-2.js` and `README.md`.

Pass arguments to your commands separated by space as you would do in the shell. See [examples](#examples) below.

## Running multiple commands in a sequence

You can run multiple commands in a sequence on every glob. To do so, pass an array of commands instead of a single one. This is useful for running autoformatting tools like `eslint --fix` or `stylefmt` but can be used for any arbitrary sequences.

For example:

```json
{
  "*.js": ["eslint", "prettier --write"]
}
```

going to execute `eslint` and if it exits with `0` code, it will execute `prettier --write` on all staged `*.js` files.

This will result in _lint-staged_ running `eslint file-1.js file-2.js`, when you have staged files `file-1.js`, `file-2.js` and `README.md`, and if it passes, `prettier --write file-1.js file-2.js`.

## Using JS configuration files

Writing the configuration file in JavaScript is the most powerful way to configure lint-staged (`lint-staged.config.js`, [similar](https://github.com/okonet/lint-staged#configuration), or passed via `--config`). From the configuration file, you can export either a single function or an object.

If the `exports` value is a function, it will receive an array of all staged filenames. You can then build your own matchers for the files and return a command string or an array of command strings. These strings are considered complete and should include the filename arguments, if wanted.

If the `exports` value is an object, its keys should be glob matches (like in the normal non-js config format). The values can either be like in the normal config or individual functions like described above. Instead of receiving all matched files, the functions in the exported object will only receive the staged files matching the corresponding glob key.

To summarize, by default _lint-staged_ automatically adds the list of matched staged files to your command, but when building the command using JS functions it is expected to do this manually. For example:

```js
export default {
  '*.js': (stagedFiles) => [`eslint .`, `prettier --write ${stagedFiles.join(' ')}`],
}
```

This will result in _lint-staged_ first running `eslint .` (matching _all_ files), and if it passes, `prettier --write file-1.js file-2.js`, when you have staged files `file-1.js`, `file-2.js` and `README.md`.

### Function signature

The function can also be async:

```ts
(filenames: string[]) => string | string[] | Promise<string | string[]>
```

### Example: Export a function to build your own matchers

<details>
  <summary>Click to expand</summary>

```js
// lint-staged.config.js
import micromatch from 'micromatch'

export default (allStagedFiles) => {
  const shFiles = micromatch(allStagedFiles, ['**/src/**/*.sh'])
  if (shFiles.length) {
    return `printf '%s\n' "Script files aren't allowed in src directory" >&2`
  }
  const codeFiles = micromatch(allStagedFiles, ['**/*.js', '**/*.ts'])
  const docFiles = micromatch(allStagedFiles, ['**/*.md'])
  return [`eslint ${codeFiles.join(' ')}`, `mdl ${docFiles.join(' ')}`]
}
```

</details>

### Example: Wrap filenames in single quotes and run once per file

<details>
  <summary>Click to expand</summary>

```js
// .lintstagedrc.js
export default {
  '**/*.js?(x)': (filenames) => filenames.map((filename) => `prettier --write '${filename}'`),
}
```

</details>

### Example: Run `tsc` on changes to TypeScript files, but do not pass any filename arguments

<details>
  <summary>Click to expand</summary>

```js
// lint-staged.config.js
export default {
  '**/*.ts?(x)': () => 'tsc -p tsconfig.json --noEmit',
}
```

</details>

### Example: Run ESLint on entire repo if more than 10 staged files

<details>
  <summary>Click to expand</summary>

```js
// .lintstagedrc.js
export default {
  '**/*.js?(x)': (filenames) =>
    filenames.length > 10 ? 'eslint .' : `eslint ${filenames.join(' ')}`,
}
```

</details>

### Example: Use your own globs

<details>
  <summary>Click to expand</summary>

It's better to use the [function-based configuration (seen above)](https://github.com/okonet/lint-staged#example-export-a-function-to-build-your-own-matchers), if your use case is this.

```js
// lint-staged.config.js
import micromatch from 'micromatch'

export default {
  '*': (allFiles) => {
    const codeFiles = micromatch(allFiles, ['**/*.js', '**/*.ts'])
    const docFiles = micromatch(allFiles, ['**/*.md'])
    return [`eslint ${codeFiles.join(' ')}`, `mdl ${docFiles.join(' ')}`]
  },
}
```

</details>

### Example: Ignore files from match

<details>
  <summary>Click to expand</summary>

If for some reason you want to ignore files from the glob match, you can use `micromatch.not()`:

```js
// lint-staged.config.js
import micromatch from 'micromatch'

export default {
  '*.js': (files) => {
    // from `files` filter those _NOT_ matching `*test.js`
    const match = micromatch.not(files, '*test.js')
    return `eslint ${match.join(' ')}`
  },
}
```

Please note that for most cases, globs can achieve the same effect. For the above example, a matching glob would be `!(*test).js`.

</details>

### Example: Use relative paths for commands

<details>
  <summary>Click to expand</summary>

```js
import path from 'path'

export default {
  '*.ts': (absolutePaths) => {
    const cwd = process.cwd()
    const relativePaths = absolutePaths.map((file) => path.relative(cwd, file))
    return `ng lint myProjectName --files ${relativePaths.join(' ')}`
  },
}
```

</details>

## Reformatting the code

Tools like [Prettier](https://prettier.io), ESLint/TSLint, or stylelint can reformat your code according to an appropriate config by running `prettier --write`/`eslint --fix`/`tslint --fix`/`stylelint --fix`. Lint-staged will automatically add any modifications to the commit as long as there are no errors.

```json
{
  "*.js": "prettier --write"
}
```

Prior to version 10, tasks had to manually include `git add` as the final step. This behavior has been integrated into lint-staged itself in order to prevent race conditions with multiple tasks editing the same files. If lint-staged detects `git add` in task configurations, it will show a warning in the console. Please remove `git add` from your configuration after upgrading.

## Examples

All examples assume you've already set up lint-staged in the `package.json` file and [husky](https://github.com/typicode/husky) in its own config file.

```json
{
  "name": "My project",
  "version": "0.1.0",
  "scripts": {
    "my-custom-script": "linter --arg1 --arg2"
  },
  "lint-staged": {}
}
```

In `.husky/pre-commit`

```shell
# .husky/pre-commit

npx lint-staged
```

_Note: we don't pass a path as an argument for the runners. This is important since lint-staged will do this for you._

### ESLint with default parameters for `*.js` and `*.jsx` running as a pre-commit hook

<details>
  <summary>Click to expand</summary>

```json
{
  "*.{js,jsx}": "eslint"
}
```

</details>

### Automatically fix code style with `--fix` and add to commit

<details>
  <summary>Click to expand</summary>

```json
{
  "*.js": "eslint --fix"
}
```

This will run `eslint --fix` and automatically add changes to the commit.

</details>

### Reuse npm script

<details>
  <summary>Click to expand</summary>

If you wish to reuse a npm script defined in your package.json:

```json
{
  "*.js": "npm run my-custom-script --"
}
```

The following is equivalent:

```json
{
  "*.js": "linter --arg1 --arg2"
}
```

</details>

### Use environment variables with linting commands

<details>
  <summary>Click to expand</summary>

Linting commands _do not_ support the shell convention of expanding environment variables. To enable the convention yourself, use a tool like [`cross-env`](https://github.com/kentcdodds/cross-env).

For example, here is `jest` running on all `.js` files with the `NODE_ENV` variable being set to `"test"`:

```json
{
  "*.js": ["cross-env NODE_ENV=test jest --bail --findRelatedTests"]
}
```

</details>

### Automatically fix code style with `prettier` for any format Prettier supports

<details>
  <summary>Click to expand</summary>

```json
{
  "*": "prettier --ignore-unknown --write"
}
```

</details>

### Automatically fix code style with `prettier` for JavaScript, TypeScript, Markdown, HTML, or CSS

<details>
  <summary>Click to expand</summary>

```json
{
  "*.{js,jsx,ts,tsx,md,html,css}": "prettier --write"
}
```

</details>

### Stylelint for CSS with defaults and for SCSS with SCSS syntax

<details>
  <summary>Click to expand</summary>

```json
{
  "*.css": "stylelint",
  "*.scss": "stylelint --syntax=scss"
}
```

</details>

### Run PostCSS sorting and Stylelint to check

<details>
  <summary>Click to expand</summary>

```json
{
  "*.scss": ["postcss --config path/to/your/config --replace", "stylelint"]
}
```

</details>

### Minify the images

<details>
  <summary>Click to expand</summary>

```json
{
  "*.{png,jpeg,jpg,gif,svg}": "imagemin-lint-staged"
}
```

<details>
  <summary>More about <code>imagemin-lint-staged</code></summary>

[imagemin-lint-staged](https://github.com/tomchentw/imagemin-lint-staged) is a CLI tool designed for lint-staged usage with sensible defaults.

See more on [this blog post](https://medium.com/@tomchentw/imagemin-lint-staged-in-place-minify-the-images-before-adding-to-the-git-repo-5acda0b4c57e) for benefits of this approach.

</details>
</details>

### Typecheck your staged files with flow

<details>
  <summary>Click to expand</summary>

```json
{
  "*.{js,jsx}": "flow focus-check"
}
```

</details>

### Integrate with Next.js

<details>
  <summary>Click to expand</summary>

```js
// .lintstagedrc.js
// See https://nextjs.org/docs/basic-features/eslint#lint-staged for details

const path = require('path')

const buildEslintCommand = (filenames) =>
  `next lint --fix --file ${filenames.map((f) => path.relative(process.cwd(), f)).join(' --file ')}`

module.exports = {
  '*.{js,jsx,ts,tsx}': [buildEslintCommand],
}
```

</details>

## Frequently Asked Questions

### The output of commit hook looks weird (no colors, duplicate lines, verbose output on Windows, …)

<details>
  <summary>Click to expand</summary>

Git 2.36.0 introduced a change to hooks where they were no longer run in the original TTY.
This was fixed in 2.37.0:

https://raw.githubusercontent.com/git/git/master/Documentation/RelNotes/2.37.0.txt

> - In Git 2.36 we revamped the way how hooks are invoked. One change
>   that is end-user visible is that the output of a hook is no longer
>   directly connected to the standard output of "git" that spawns the
>   hook, which was noticed post release. This is getting corrected.
>   (merge [a082345372](https://github.com/git/git/commit/a082345372) ab/hooks-regression-fix later to maint).

If updating Git doesn't help, you can try to manually redirect the output in your Git hook; for example:

```shell
# .husky/pre-commit

if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then exec >/dev/tty 2>&1; fi

npx lint-staged
```

Source: https://github.com/typicode/husky/issues/968#issuecomment-1176848345

</details>

### Can I use `lint-staged` via node?

<details>
  <summary>Click to expand</summary>

Yes!

```js
import lintStaged from 'lint-staged'

try {
  const success = await lintStaged()
  console.log(success ? 'Linting was successful!' : 'Linting failed!')
} catch (e) {
  // Failed to load configuration
  console.error(e)
}
```

Parameters to `lintStaged` are equivalent to their CLI counterparts:

```js
const success = await lintStaged({
  allowEmpty: false,
  concurrent: true,
  configPath: './path/to/configuration/file',
  cwd: process.cwd(),
  debug: false,
  maxArgLength: null,
  quiet: false,
  relative: false,
  shell: false,
  stash: true,
  verbose: false,
})
```

You can also pass config directly with `config` option:

```js
const success = await lintStaged({
  allowEmpty: false,
  concurrent: true,
  config: { '*.js': 'eslint --fix' },
  cwd: process.cwd(),
  debug: false,
  maxArgLength: null,
  quiet: false,
  relative: false,
  shell: false,
  stash: true,
  verbose: false,
})
```

The `maxArgLength` option configures chunking of tasks into multiple parts that are run one after the other. This is to avoid issues on Windows platforms where the maximum length of the command line argument string is limited to 8192 characters. Lint-staged might generate a very long argument string when there are many staged files. This option is set automatically from the cli, but not via the Node.js API by default.

</details>

### Using with JetBrains IDEs _(WebStorm, PyCharm, IntelliJ IDEA, RubyMine, etc.)_

<details>
  <summary>Click to expand</summary>

_**Update**_: The latest version of JetBrains IDEs now support running hooks as you would expect.

When using the IDE's GUI to commit changes with the `precommit` hook, you might see inconsistencies in the IDE and command line. This is [known issue](https://youtrack.jetbrains.com/issue/IDEA-135454) at JetBrains so if you want this fixed, please vote for it on YouTrack.

Until the issue is resolved in the IDE, you can use the following config to work around it:

husky v1.x

```json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "post-commit": "git update-index --again"
    }
  }
}
```

husky v0.x

```json
{
  "scripts": {
    "precommit": "lint-staged",
    "postcommit": "git update-index --again"
  }
}
```

_Thanks to [this comment](https://youtrack.jetbrains.com/issue/IDEA-135454#comment=27-2710654) for the fix!_

</details>

### How to use `lint-staged` in a multi-package monorepo?

<details>
  <summary>Click to expand</summary>

Install _lint-staged_ on the monorepo root level, and add separate configuration files in each package. When running, _lint-staged_ will always use the configuration closest to a staged file, so having separate configuration files makes sure linters do not "leak" into other packages.

For example, in a monorepo with `packages/frontend/.lintstagedrc.json` and `packages/backend/.lintstagedrc.json`, a staged file inside `packages/frontend/` will only match that configuration, and not the one in `packages/backend/`.

**Note**: _lint-staged_ discovers the closest configuration to each staged file, even if that configuration doesn't include any matching globs. Given these example configurations:

```js
// ./.lintstagedrc.json
{ "*.md": "prettier --write" }
```

```js
// ./packages/frontend/.lintstagedrc.json
{ "*.js": "eslint --fix" }
```

When committing `./packages/frontend/README.md`, it **will not run** _prettier_, because the configuration in the `frontend/` directory is closer to the file and doesn't include it. You should treat all _lint-staged_ configuration files as isolated and separated from each other. You can always use JS files to "extend" configurations, for example:

```js
import baseConfig from '../.lintstagedrc.js'

export default {
  ...baseConfig,
  '*.js': 'eslint --fix',
}
```

To support backwards-compatibility, monorepo features require multiple _lint-staged_ configuration files present in the git repo. If you still want to run _lint-staged_ in only one of the packages in a monorepo, you can either add an "empty" _lint-staged_ configuration to the root of the repo (so that there's two configs in total), or alternatively run _lint-staged_ with the `--cwd` option pointing to your package directory (for example, `lint-staged --cwd packages/frontend`).

</details>

### Can I lint files outside of the current project folder?

<details>
  <summary>Click to expand</summary>

tl;dr: Yes, but the pattern should start with `../`.

By default, `lint-staged` executes linters only on the files present inside the project folder(where `lint-staged` is installed and run from).
So this question is relevant _only_ when the project folder is a child folder inside the git repo.
In certain project setups, it might be desirable to bypass this restriction. See [#425](https://github.com/okonet/lint-staged/issues/425), [#487](https://github.com/okonet/lint-staged/issues/487) for more context.

`lint-staged` provides an escape hatch for the same(`>= v7.3.0`). For patterns that start with `../`, all the staged files are allowed to match against the pattern.
Note that patterns like `*.js`, `**/*.js` will still only match the project files and not any of the files in parent or sibling directories.

Example repo: [sudo-suhas/lint-staged-django-react-demo](https://github.com/sudo-suhas/lint-staged-django-react-demo).

</details>

### Can I run `lint-staged` in CI, or when there are no staged files?

<details>
  <summary>Click to expand</summary>

Lint-staged will by default run against files staged in git, and should be run during the git pre-commit hook, for example. It's also possible to override this default behaviour and run against files in a specific diff, for example
all changed files between two different branches. If you want to run _lint-staged_ in the CI, maybe you can set it up to compare the branch in a _Pull Request_/_Merge Request_ to the target branch.

Try out the `git diff` command until you are satisfied with the result, for example:

```
git diff --diff-filter=ACMR --name-only master...my-branch
```

This will print a list of _added_, _changed_, _modified_, and _renamed_ files between `master` and `my-branch`.

You can then run lint-staged against the same files with:

```
npx lint-staged --diff="master...my-branch"
```

</details>

### Can I use `lint-staged` with `ng lint`

<details>
  <summary>Click to expand</summary>

You should not use `ng lint` through _lint-staged_, because it's designed to lint an entire project. Instead, you can add `ng lint` to your git pre-commit hook the same way as you would run lint-staged.

See issue [!951](https://github.com/okonet/lint-staged/issues/951) for more details and possible workarounds.

</details>

### How can I ignore files from `.eslintignore`?

<details>
  <summary>Click to expand</summary>

ESLint throws out `warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override` warnings that breaks the linting process ( if you used `--max-warnings=0` which is recommended ).

#### ESLint < 7

<details>
  <summary>Click to expand</summary>

Based on the discussion from [this issue](https://github.com/eslint/eslint/issues/9977), it was decided that using [the outlined script](https://github.com/eslint/eslint/issues/9977#issuecomment-406420893)is the best route to fix this.

So you can setup a `.lintstagedrc.js` config file to do this:

```js
import { CLIEngine } from 'eslint'

export default {
  '*.js': (files) => {
    const cli = new CLIEngine({})
    return 'eslint --max-warnings=0 ' + files.filter((file) => !cli.isPathIgnored(file)).join(' ')
  },
}
```

</details>

#### ESLint >= 7

<details>
  <summary>Click to expand</summary>

In versions of ESLint > 7, [isPathIgnored](https://eslint.org/docs/developer-guide/nodejs-api#-eslintispathignoredfilepath) is an async function and now returns a promise. The code below can be used to reinstate the above functionality.

Since [10.5.3](https://github.com/okonet/lint-staged/releases), any errors due to a bad ESLint config will come through to the console.

```js
import { ESLint } from 'eslint'

const removeIgnoredFiles = async (files) => {
  const eslint = new ESLint()
  const isIgnored = await Promise.all(
    files.map((file) => {
      return eslint.isPathIgnored(file)
    })
  )
  const filteredFiles = files.filter((_, i) => !isIgnored[i])
  return filteredFiles.join(' ')
}

export default {
  '**/*.{ts,tsx,js,jsx}': async (files) => {
    const filesToLint = await removeIgnoredFiles(files)
    return [`eslint --max-warnings=0 ${filesToLint}`]
  },
}
```

</details>

#### ESLint >= 8.51.0 && [Flat ESLint config](https://eslint.org/docs/latest/use/configure/configuration-files-new)

<details>
  <summary>Click to expand</summary>

ESLint v8.51.0 introduced [`--no-warn-ignored` CLI flag](https://eslint.org/docs/latest/use/command-line-interface#--no-warn-ignored). It suppresses the `warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override` warning, so manually ignoring files via `eslint.isPathIgnored` is no longer necessary.

```json
{
  "*.js": "eslint --max-warnings=0 --no-warn-ignored"
}
```

**NOTE:** `--no-warn-ignored` flag is only available when [Flat ESLint config](https://eslint.org/docs/latest/use/configure/configuration-files-new) is used.

</details>

</details>


code:
#!/usr/bin/env node

import fs from 'node:fs/promises'

import { supportsColor } from 'chalk'
import { Option, program } from 'commander'
import debug from 'debug'

import lintStaged from '../lib/index.js'
import { CONFIG_STDIN_ERROR } from '../lib/messages.js'
import { readStdin } from '../lib/readStdin.js'

// Force colors for packages that depend on https://www.npmjs.com/package/supports-color
if (supportsColor) {
  process.env.FORCE_COLOR = supportsColor.level.toString()
}

const debugLog = debug('lint-staged:bin')

// Do not terminate main Listr process on SIGINT
process.on('SIGINT', () => {})

const packageJson = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url)))
const version = packageJson.version

const cli = program.version(version)

cli.option('--allow-empty', 'allow empty commits when tasks revert all staged changes', false)

cli.option(
  '-p, --concurrent <number|boolean>',
  'the number of tasks to run concurrently, or false for serial',
  true
)

cli.option('-c, --config [path]', 'path to configuration file, or - to read from stdin')

cli.option('--cwd [path]', 'run all tasks in specific directory, instead of the current')

cli.option('-d, --debug', 'print additional debug information', false)

cli.addOption(
  new Option(
    '--diff [string]',
    'override the default "--staged" flag of "git diff" to get list of files. Implies "--no-stash".'
  ).implies({ stash: false })
)

cli.option(
  '--diff-filter [string]',
  'override the default "--diff-filter=ACMR" flag of "git diff" to get list of files'
)

cli.option('--max-arg-length [number]', 'maximum length of the command-line argument string', 0)

/**
 * We don't want to show the `--stash` flag because it's on by default, and only show the
 * negatable flag `--no-stash` in stead. There seems to be a bug in Commander.js where
 * configuring only the latter won't actually set the default value.
 */
cli
  .addOption(
    new Option('--stash', 'enable the backup stash, and revert in case of errors')
      .default(true)
      .hideHelp()
  )
  .addOption(
    new Option(
      '--no-stash',
      'disable the backup stash, and do not revert in case of errors. Implies "--no-hide-partially-staged".'
    )
      .default(false)
      .implies({ hidePartiallyStaged: false })
  )

/**
 * We don't want to show the `--hide-partially-staged` flag because it's on by default, and only show the
 * negatable flag `--no-hide-partially-staged` in stead. There seems to be a bug in Commander.js where
 * configuring only the latter won't actually set the default value.
 */
cli
  .addOption(
    new Option('--hide-partially-staged', 'hide unstaged changes from partially staged files')
      .default(true)
      .hideHelp()
  )
  .addOption(
    new Option(
      '--no-hide-partially-staged',
      'disable hiding unstaged changes from partially staged files'
    ).default(false)
  )

cli.option('-q, --quiet', 'disable lint-staged’s own console output', false)

cli.option('-r, --relative', 'pass relative filepaths to tasks', false)

cli.option('-x, --shell [path]', 'skip parsing of tasks for better shell support', false)

cli.option(
  '-v, --verbose',
  'show task output even when tasks succeed; by default only failed output is shown',
  false
)

const cliOptions = cli.parse(process.argv).opts()

if (cliOptions.debug) {
  debug.enable('lint-staged*')
}

debugLog('Running `lint-staged@%s` on Node.js %s (%s)', version, process.version, process.platform)

const options = {
  allowEmpty: !!cliOptions.allowEmpty,
  concurrent: JSON.parse(cliOptions.concurrent),
  configPath: cliOptions.config,
  cwd: cliOptions.cwd,
  debug: !!cliOptions.debug,
  diff: cliOptions.diff,
  diffFilter: cliOptions.diffFilter,
  maxArgLength: cliOptions.maxArgLength || undefined,
  quiet: !!cliOptions.quiet,
  relative: !!cliOptions.relative,
  shell: cliOptions.shell /* Either a boolean or a string pointing to the shell */,
  stash: !!cliOptions.stash, // commander inverts `no-<x>` flags to `!x`
  hidePartiallyStaged: !!cliOptions.hidePartiallyStaged, // commander inverts `no-<x>` flags to `!x`
  verbose: !!cliOptions.verbose,
}

debugLog('Options parsed from command-line: %o', options)

if (options.configPath === '-') {
  delete options.configPath
  try {
    debugLog('Reading config from stdin')
    options.config = JSON.parse(await readStdin())
  } catch (error) {
    debugLog(CONFIG_STDIN_ERROR, error)
    console.error(CONFIG_STDIN_ERROR)
    process.exit(1)
  }
}

try {
  const passed = await lintStaged(options)
  process.exitCode = passed ? 0 : 1
} catch {
  process.exitCode = 1
}

license: mit


code:
import path from 'node:path'

import debug from 'debug'

import { normalizePath } from './normalizePath.js'

const debugLog = debug('lint-staged:chunkFiles')

/**
 * Chunk array into sub-arrays
 * @param {Array} arr
 * @param {Number} chunkCount
 * @retuns {Array<Array>}
 */
const chunkArray = (arr, chunkCount) => {
  if (chunkCount === 1) return [arr]
  const chunked = []
  let position = 0
  for (let i = 0; i < chunkCount; i++) {
    const chunkLength = Math.ceil((arr.length - position) / (chunkCount - i))
    chunked.push([])
    chunked[i] = arr.slice(position, chunkLength + position)
    position += chunkLength
  }
  return chunked
}

/**
 * Chunk files into sub-arrays based on the length of the resulting argument string
 * @param {Object} opts
 * @param {Array<String>} opts.files
 * @param {String} [opts.baseDir] The optional base directory to resolve relative paths.
 * @param {number} [opts.maxArgLength] the maximum argument string length
 * @param {Boolean} [opts.relative] whether files are relative to `topLevelDir` or should be resolved as absolute
 * @returns {Array<Array<String>>}
 */
export const chunkFiles = ({ files, baseDir, maxArgLength = null, relative = false }) => {
  const normalizedFiles = files.map((file) =>
    normalizePath(relative || !baseDir ? file : path.resolve(baseDir, file))
  )

  if (!maxArgLength) {
    debugLog('Skip chunking files because of undefined maxArgLength')
    return [normalizedFiles] // wrap in an array to return a single chunk
  }

  const fileListLength = normalizedFiles.join(' ').length
  debugLog(
    `Resolved an argument string length of ${fileListLength} characters from ${normalizedFiles.length} files`
  )
  const chunkCount = Math.min(Math.ceil(fileListLength / maxArgLength), normalizedFiles.length)
  debugLog(`Creating ${chunkCount} chunks for maxArgLength of ${maxArgLength}`)
  return chunkArray(normalizedFiles, chunkCount)
}


code:
export const CONFIG_NAME = 'lint-staged'

export const PACKAGE_JSON_FILE = 'package.json'

export const PACKAGE_YAML_FILES = ['package.yaml', 'package.yml']

/**
 * The list of files `lint-staged` will read configuration
 * from, in the declared order.
 */
export const CONFIG_FILE_NAMES = [
  PACKAGE_JSON_FILE,
  ...PACKAGE_YAML_FILES,
  '.lintstagedrc',
  '.lintstagedrc.json',
  '.lintstagedrc.yaml',
  '.lintstagedrc.yml',
  '.lintstagedrc.mjs',
  '.lintstagedrc.js',
  '.lintstagedrc.cjs',
  'lint-staged.config.mjs',
  'lint-staged.config.js',
  'lint-staged.config.cjs',
]


code:
import { pathToFileURL } from 'node:url'

export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)


code:
import debug from 'debug'
import { execa } from 'execa'

const debugLog = debug('lint-staged:execGit')

/**
 * Explicitly never recurse commands into submodules, overriding local/global configuration.
 * @see https://git-scm.com/docs/git-config#Documentation/git-config.txt-submodulerecurse
 */
const NO_SUBMODULE_RECURSE = ['-c', 'submodule.recurse=false']

// exported for tests
export const GIT_GLOBAL_OPTIONS = [...NO_SUBMODULE_RECURSE]

export const execGit = async (cmd, options = {}) => {
  debugLog('Running git command', cmd)
  try {
    const { stdout } = await execa('git', GIT_GLOBAL_OPTIONS.concat(cmd), {
      ...options,
      all: true,
      cwd: options.cwd || process.cwd(),
      stdin: 'ignore',
    })
    return stdout
  } catch ({ all }) {
    throw new Error(all)
  }
}


code:
import chalk from 'chalk'
import { figures } from 'listr2'

export const info = chalk.blue(figures.arrowRight)

export const error = chalk.redBright(figures.cross)

export const warning = chalk.yellow(figures.warning)


code:
import fs from 'node:fs/promises'

import debug from 'debug'

const debugLog = debug('lint-staged:file')

/**
 * Read contents of a file to buffer
 * @param {String} filename
 * @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist
 * @returns {Promise<Buffer>}
 */
export const readFile = async (filename, ignoreENOENT = true) => {
  debugLog('Reading file `%s`', filename)
  try {
    return await fs.readFile(filename)
  } catch (error) {
    if (ignoreENOENT && error.code === 'ENOENT') {
      debugLog("File `%s` doesn't exist, ignoring...", filename)
      return null // no-op file doesn't exist
    } else {
      throw error
    }
  }
}

/**
 * Remove a file
 * @param {String} filename
 * @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist
 */
export const unlink = async (filename, ignoreENOENT = true) => {
  debugLog('Removing file `%s`', filename)
  try {
    await fs.unlink(filename)
  } catch (error) {
    if (ignoreENOENT && error.code === 'ENOENT') {
      debugLog("File `%s` doesn't exist, ignoring...", filename)
    } else {
      throw error
    }
  }
}

/**
 * Write buffer to file
 * @param {String} filename
 * @param {Buffer} buffer
 */
export const writeFile = async (filename, buffer) => {
  debugLog('Writing file `%s`', filename)
  await fs.writeFile(filename, buffer)
}


code:
import path from 'node:path'

import debug from 'debug'
import micromatch from 'micromatch'

import { normalizePath } from './normalizePath.js'

const debugLog = debug('lint-staged:generateTasks')

/**
 * Generates all task commands, and filelist
 *
 * @param {object} options
 * @param {Object} [options.config] - Task configuration
 * @param {Object} [options.cwd] - Current working directory
 * @param {boolean} [options.files] - Staged filepaths
 * @param {boolean} [options.relative] - Whether filepaths to should be relative to cwd
 */
export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => {
  debugLog('Generating linter tasks')

  const relativeFiles = files.map((file) => normalizePath(path.relative(cwd, file)))

  return Object.entries(config).map(([pattern, commands]) => {
    const isParentDirPattern = pattern.startsWith('../')

    // Only worry about children of the CWD unless the pattern explicitly
    // specifies that it concerns a parent directory.
    const filteredFiles = relativeFiles.filter((file) => {
      if (isParentDirPattern) return true
      return !file.startsWith('..') && !path.isAbsolute(file)
    })

    const matches = micromatch(filteredFiles, pattern, {
      cwd,
      dot: true,
      // If the pattern doesn't look like a path, enable `matchBase` to
      // match against filenames in every directory. This makes `*.js`
      // match both `test.js` and `subdirectory/test.js`.
      matchBase: !pattern.includes('/'),
      posixSlashes: true,
      strictBrackets: true,
    })

    const fileList = matches.map((file) => normalizePath(relative ? file : path.resolve(cwd, file)))

    const task = { pattern, commands, fileList }
    debugLog('Generated task: \n%O', task)

    return task
  })
}


code:
export function getDiffCommand(diff, diffFilter) {
  /**
   *  Docs for --diff-filter option:
   * @see https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203
   */
  const diffFilterArg = diffFilter !== undefined ? diffFilter.trim() : 'ACMR'

  /** Use `--diff branch1...branch2` or `--diff="branch1 branch2", or fall back to default staged files */
  const diffArgs = diff !== undefined ? diff.trim().split(' ') : ['--staged']

  /**
   * Docs for -z option:
   * @see https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z
   */
  const diffCommand = ['diff', '--name-only', '-z', `--diff-filter=${diffFilterArg}`, ...diffArgs]

  return diffCommand
}


code:
import { EOL } from 'node:os'
import { Writable } from 'node:stream'

import { ListrLogger, ProcessOutput } from 'listr2'

const EOLRegex = new RegExp(EOL + '$')

const bindLogger = (consoleLogMethod) =>
  new Writable({
    write: function (chunk, encoding, next) {
      consoleLogMethod(chunk.toString().replace(EOLRegex, ''))
      next()
    },
  })

const getMainRendererOptions = ({ debug, quiet }, logger, env) => {
  if (quiet) {
    return {
      renderer: 'silent',
    }
  }

  if (env.NODE_ENV === 'test') {
    return {
      renderer: 'test',
      rendererOptions: {
        logger: new ListrLogger({
          processOutput: new ProcessOutput(bindLogger(logger.log), bindLogger(logger.error)),
        }),
      },
    }
  }

  // Better support for dumb terminals: https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals
  if (debug || env.TERM === 'dumb') {
    return {
      renderer: 'verbose',
    }
  }

  return {
    renderer: 'update',
    rendererOptions: {
      formatOutput: 'truncate',
    },
  }
}

const getFallbackRenderer = ({ renderer }, { FORCE_COLOR }) => {
  if (renderer === 'silent' || renderer === 'test' || Number(FORCE_COLOR) > 0) {
    return renderer
  }

  return 'verbose'
}

export const getRenderer = (options, logger, env = process.env) => {
  const mainRendererOptions = getMainRendererOptions(options, logger, env)

  return {
    ...mainRendererOptions,
    fallbackRenderer: getFallbackRenderer(mainRendererOptions, env),
  }
}


code:
import path from 'node:path'

import { execGit } from './execGit.js'
import { getDiffCommand } from './getDiffCommand.js'
import { normalizePath } from './normalizePath.js'
import { parseGitZOutput } from './parseGitZOutput.js'

export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } = {}) => {
  try {
    const lines = await execGit(getDiffCommand(diff, diffFilter), { cwd })
    if (!lines) return []

    return parseGitZOutput(lines).map((file) => normalizePath(path.resolve(cwd, file)))
  } catch {
    return null
  }
}


code:
import path from 'node:path'

import debug from 'debug'

import { execGit } from './execGit.js'
import { readFile, unlink, writeFile } from './file.js'
import { getDiffCommand } from './getDiffCommand.js'
import {
  ApplyEmptyCommitError,
  GetBackupStashError,
  GitError,
  HideUnstagedChangesError,
  RestoreMergeStatusError,
  RestoreOriginalStateError,
  RestoreUnstagedChangesError,
} from './symbols.js'

const debugLog = debug('lint-staged:GitWorkflow')

const MERGE_HEAD = 'MERGE_HEAD'
const MERGE_MODE = 'MERGE_MODE'
const MERGE_MSG = 'MERGE_MSG'

// In git status machine output, renames are presented as `to`NUL`from`
// When diffing, both need to be taken into account, but in some cases on the `to`.
// eslint-disable-next-line no-control-regex
const RENAME = /\x00/

/**
 * From list of files, split renames and flatten into two files `to`NUL`from`.
 * @param {string[]} files
 * @param {Boolean} [includeRenameFrom=true] Whether or not to include the `from` renamed file, which is no longer on disk
 */
const processRenames = (files, includeRenameFrom = true) =>
  files.reduce((flattened, file) => {
    if (RENAME.test(file)) {
      const [to, from] = file.split(RENAME)
      if (includeRenameFrom) flattened.push(from)
      flattened.push(to)
    } else {
      flattened.push(file)
    }
    return flattened
  }, [])

export const STASH = 'lint-staged automatic backup'

const PATCH_UNSTAGED = 'lint-staged_unstaged.patch'

const GIT_DIFF_ARGS = [
  '--binary', // support binary files
  '--unified=0', // do not add lines around diff for consistent behaviour
  '--no-color', // disable colors for consistent behaviour
  '--no-ext-diff', // disable external diff tools for consistent behaviour
  '--src-prefix=a/', // force prefix for consistent behaviour
  '--dst-prefix=b/', // force prefix for consistent behaviour
  '--patch', // output a patch that can be applied
  '--submodule=short', // always use the default short format for submodules
]
const GIT_APPLY_ARGS = ['-v', '--whitespace=nowarn', '--recount', '--unidiff-zero']

const handleError = (error, ctx, symbol) => {
  ctx.errors.add(GitError)
  if (symbol) ctx.errors.add(symbol)
  throw error
}

export class GitWorkflow {
  constructor({ allowEmpty, gitConfigDir, topLevelDir, matchedFileChunks, diff, diffFilter }) {
    this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: topLevelDir })
    this.deletedFiles = []
    this.gitConfigDir = gitConfigDir
    this.topLevelDir = topLevelDir
    this.diff = diff
    this.diffFilter = diffFilter
    this.allowEmpty = allowEmpty
    this.matchedFileChunks = matchedFileChunks

    /**
     * These three files hold state about an ongoing git merge
     * Resolve paths during constructor
     */
    this.mergeHeadFilename = path.resolve(gitConfigDir, MERGE_HEAD)
    this.mergeModeFilename = path.resolve(gitConfigDir, MERGE_MODE)
    this.mergeMsgFilename = path.resolve(gitConfigDir, MERGE_MSG)
  }

  /**
   * Get absolute path to file hidden inside .git
   * @param {string} filename
   */
  getHiddenFilepath(filename) {
    return path.resolve(this.gitConfigDir, `./${filename}`)
  }

  /**
   * Get name of backup stash
   */
  async getBackupStash(ctx) {
    const stashes = await this.execGit(['stash', 'list'])
    const index = stashes.split('\n').findIndex((line) => line.includes(STASH))
    if (index === -1) {
      ctx.errors.add(GetBackupStashError)
      throw new Error('lint-staged automatic backup is missing!')
    }

    return String(index)
  }

  /**
   * Get a list of unstaged deleted files
   */
  async getDeletedFiles() {
    debugLog('Getting deleted files...')
    const lsFiles = await this.execGit(['ls-files', '--deleted'])
    const deletedFiles = lsFiles
      .split('\n')
      .filter(Boolean)
      .map((file) => path.resolve(this.topLevelDir, file))
    debugLog('Found deleted files:', deletedFiles)
    return deletedFiles
  }

  /**
   * Save meta information about ongoing git merge
   */
  async backupMergeStatus() {
    debugLog('Backing up merge state...')
    await Promise.all([
      readFile(this.mergeHeadFilename).then((buffer) => (this.mergeHeadBuffer = buffer)),
      readFile(this.mergeModeFilename).then((buffer) => (this.mergeModeBuffer = buffer)),
      readFile(this.mergeMsgFilename).then((buffer) => (this.mergeMsgBuffer = buffer)),
    ])
    debugLog('Done backing up merge state!')
  }

  /**
   * Restore meta information about ongoing git merge
   */
  async restoreMergeStatus(ctx) {
    debugLog('Restoring merge state...')
    try {
      await Promise.all([
        this.mergeHeadBuffer && writeFile(this.mergeHeadFilename, this.mergeHeadBuffer),
        this.mergeModeBuffer && writeFile(this.mergeModeFilename, this.mergeModeBuffer),
        this.mergeMsgBuffer && writeFile(this.mergeMsgFilename, this.mergeMsgBuffer),
      ])
      debugLog('Done restoring merge state!')
    } catch (error) {
      debugLog('Failed restoring merge state with error:')
      debugLog(error)
      handleError(
        new Error('Merge state could not be restored due to an error!'),
        ctx,
        RestoreMergeStatusError
      )
    }
  }

  /**
   * Get a list of all files with both staged and unstaged modifications.
   * Renames have special treatment, since the single status line includes
   * both the "from" and "to" filenames, where "from" is no longer on disk.
   */
  async getPartiallyStagedFiles() {
    debugLog('Getting partially staged files...')
    const status = await this.execGit(['status', '-z'])
    /**
     * See https://git-scm.com/docs/git-status#_short_format
     * Entries returned in machine format are separated by a NUL character.
     * The first letter of each entry represents current index status,
     * and second the working tree. Index and working tree status codes are
     * separated from the file name by a space. If an entry includes a
     * renamed file, the file names are separated by a NUL character
     * (e.g. `to`\0`from`)
     */
    const partiallyStaged = status
      // eslint-disable-next-line no-control-regex
      .split(/\x00(?=[ AMDRCU?!]{2} |$)/)
      .filter((line) => {
        const [index, workingTree] = line
        return index !== ' ' && workingTree !== ' ' && index !== '?' && workingTree !== '?'
      })
      .map((line) => line.substr(3)) // Remove first three letters (index, workingTree, and a whitespace)
      .filter(Boolean) // Filter empty string
    debugLog('Found partially staged files:', partiallyStaged)
    return partiallyStaged.length ? partiallyStaged : null
  }

  /**
   * Create a diff of partially staged files and backup stash if enabled.
   */
  async prepare(ctx) {
    try {
      debugLog('Backing up original state...')

      // Get a list of files with bot staged and unstaged changes.
      // Unstaged changes to these files should be hidden before the tasks run.
      this.partiallyStagedFiles = await this.getPartiallyStagedFiles()

      if (this.partiallyStagedFiles) {
        ctx.hasPartiallyStagedFiles = true
        const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
        const files = processRenames(this.partiallyStagedFiles)
        await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
      } else {
        ctx.hasPartiallyStagedFiles = false
      }

      /**
       * If backup stash should be skipped, no need to continue
       */
      if (!ctx.shouldBackup) return

      // When backup is enabled, the revert will clear ongoing merge status.
      await this.backupMergeStatus()

      // Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
      // - in git versions =< 2.13.0 the `git stash --keep-index` option resurrects deleted files
      // - git stash can't infer RD or MD states correctly, and will lose the deletion
      this.deletedFiles = await this.getDeletedFiles()

      // Save stash of all staged files.
      // The `stash create` command creates a dangling commit without removing any files,
      // and `stash store` saves it as an actual stash.
      const hash = await this.execGit(['stash', 'create'])
      await this.execGit(['stash', 'store', '--quiet', '--message', STASH, hash])

      debugLog('Done backing up original state!')
    } catch (error) {
      handleError(error, ctx)
    }
  }

  /**
   * Remove unstaged changes to all partially staged files, to avoid tasks from seeing them
   */
  async hideUnstagedChanges(ctx) {
    try {
      const files = processRenames(this.partiallyStagedFiles, false)
      await this.execGit(['checkout', '--force', '--', ...files])
    } catch (error) {
      /**
       * `git checkout --force` doesn't throw errors, so it shouldn't be possible to get here.
       * If this does fail, the handleError method will set ctx.gitError and lint-staged will fail.
       */
      handleError(error, ctx, HideUnstagedChangesError)
    }
  }

  /**
   * Applies back task modifications, and unstaged changes hidden in the stash.
   * In case of a merge-conflict retry with 3-way merge.
   */
  async applyModifications(ctx) {
    debugLog('Adding task modifications to index...')

    // `matchedFileChunks` includes staged files that lint-staged originally detected and matched against a task.
    // Add only these files so any 3rd-party edits to other files won't be included in the commit.
    // These additions per chunk are run "serially" to prevent race conditions.
    // Git add creates a lockfile in the repo causing concurrent operations to fail.
    for (const files of this.matchedFileChunks) {
      await this.execGit(['add', '--', ...files])
    }

    debugLog('Done adding task modifications to index!')

    const stagedFilesAfterAdd = await this.execGit(getDiffCommand(this.diff, this.diffFilter))
    if (!stagedFilesAfterAdd && !this.allowEmpty) {
      // Tasks reverted all staged changes and the commit would be empty
      // Throw error to stop commit unless `--allow-empty` was used
      handleError(new Error('Prevented an empty git commit!'), ctx, ApplyEmptyCommitError)
    }
  }

  /**
   * Restore unstaged changes to partially changed files. If it at first fails,
   * this is probably because of conflicts between new task modifications.
   * 3-way merge usually fixes this, and in case it doesn't we should just give up and throw.
   */
  async restoreUnstagedChanges(ctx) {
    debugLog('Restoring unstaged changes...')
    const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
    try {
      await this.execGit(['apply', ...GIT_APPLY_ARGS, unstagedPatch])
    } catch (applyError) {
      debugLog('Error while restoring changes:')
      debugLog(applyError)
      debugLog('Retrying with 3-way merge')
      try {
        // Retry with a 3-way merge if normal apply fails
        await this.execGit(['apply', ...GIT_APPLY_ARGS, '--3way', unstagedPatch])
      } catch (threeWayApplyError) {
        debugLog('Error while restoring unstaged changes using 3-way merge:')
        debugLog(threeWayApplyError)
        handleError(
          new Error('Unstaged changes could not be restored due to a merge conflict!'),
          ctx,
          RestoreUnstagedChangesError
        )
      }
    }
  }

  /**
   * Restore original HEAD state in case of errors
   */
  async restoreOriginalState(ctx) {
    try {
      debugLog('Restoring original state...')
      await this.execGit(['reset', '--hard', 'HEAD'])
      await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash(ctx)])

      // Restore meta information about ongoing git merge
      await this.restoreMergeStatus(ctx)

      // If stashing resurrected deleted files, clean them out
      await Promise.all(this.deletedFiles.map((file) => unlink(file)))

      // Clean out patch
      await unlink(this.getHiddenFilepath(PATCH_UNSTAGED))

      debugLog('Done restoring original state!')
    } catch (error) {
      handleError(error, ctx, RestoreOriginalStateError)
    }
  }

  /**
   * Drop the created stashes after everything has run
   */
  async cleanup(ctx) {
    try {
      debugLog('Dropping backup stash...')
      await this.execGit(['stash', 'drop', '--quiet', await this.getBackupStash(ctx)])
      debugLog('Done dropping backup stash!')
    } catch (error) {
      handleError(error, ctx)
    }
  }
}


code:
import path from 'node:path'

import debug from 'debug'

const debugLog = debug('lint-staged:groupFilesByConfig')

export const groupFilesByConfig = async ({ configs, files, singleConfigMode }) => {
  debugLog('Grouping %d files by %d configurations', files.length, Object.keys(configs).length)

  const filesSet = new Set(files)
  const filesByConfig = {}

  /** Configs are sorted deepest first by `searchConfigs` */
  for (const [filepath, config] of Object.entries(configs)) {
    /** When passed an explicit config object via the Node.js API‚ or an explicit path, skip logic */
    if (singleConfigMode) {
      filesByConfig[filepath] = { config, files }
      break
    }

    const dir = path.normalize(path.dirname(filepath))

    /** Check if file is inside directory of the configuration file */
    const isInsideDir = (file) => {
      const relative = path.relative(dir, file)
      return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
    }

    /** This config should match all files since it has a parent glob */
    const includeAllFiles = Object.keys(config).some((glob) => glob.startsWith('..'))

    const scopedFiles = new Set(includeAllFiles ? filesSet : undefined)

    /**
     * Without a parent glob, if file is inside the config file's directory,
     * assign it to that configuration.
     */
    if (!includeAllFiles) {
      filesSet.forEach((file) => {
        if (isInsideDir(file)) {
          scopedFiles.add(file)
        }
      })
    }

    /** Files should only match a single config */
    scopedFiles.forEach((file) => {
      filesSet.delete(file)
    })

    filesByConfig[filepath] = { config, files: Array.from(scopedFiles) }
  }

  return filesByConfig
}


code:
import debug from 'debug'

import { execGit } from './execGit.js'
import {
  GIT_ERROR,
  NO_CONFIGURATION,
  PREVENTED_EMPTY_COMMIT,
  RESTORE_STASH_EXAMPLE,
} from './messages.js'
import { printTaskOutput } from './printTaskOutput.js'
import { runAll } from './runAll.js'
import {
  ApplyEmptyCommitError,
  ConfigNotFoundError,
  GetBackupStashError,
  GitError,
} from './symbols.js'
import { validateOptions } from './validateOptions.js'

const debugLog = debug('lint-staged')

/**
 * Get the maximum length of a command-line argument string based on current platform
 *
 * https://serverfault.com/questions/69430/what-is-the-maximum-length-of-a-command-line-in-mac-os-x
 * https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation
 * https://unix.stackexchange.com/a/120652
 */
const getMaxArgLength = () => {
  switch (process.platform) {
    case 'darwin':
      return 262144
    case 'win32':
      return 8191
    default:
      return 131072
  }
}

/**
 * @typedef {(...any) => void} LogFunction
 * @typedef {{ error: LogFunction, log: LogFunction, warn: LogFunction }} Logger
 *
 * Root lint-staged function that is called from `bin/lint-staged`.
 *
 * @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] - Object with configuration for programmatic API
 * @param {string} [options.configPath] - Path to configuration file
 * @param {Object} [options.cwd] - Current working directory
 * @param {boolean} [options.debug] - Enable debug mode
 * @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files
 * @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files
 * @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|string} [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<boolean>} Promise of whether the linting passed or failed
 */
const lintStaged = async (
  {
    allowEmpty = false,
    concurrent = true,
    config: configObject,
    configPath,
    cwd,
    debug = false,
    diff,
    diffFilter,
    maxArgLength = getMaxArgLength() / 2,
    quiet = false,
    relative = false,
    shell = false,
    // Stashing should be disabled by default when the `diff` option is used
    stash = diff === undefined,
    hidePartiallyStaged = stash,
    verbose = false,
  } = {},
  logger = console
) => {
  const gitVersion = await execGit(['version', '--build-options'], { cwd })
  debugLog('%s', gitVersion)

  const options = {
    allowEmpty,
    concurrent,
    configObject,
    configPath,
    cwd,
    debug,
    diff,
    diffFilter,
    maxArgLength,
    quiet,
    relative,
    shell,
    stash,
    hidePartiallyStaged,
    verbose,
  }

  await validateOptions(options, logger)

  // Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
  debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
  delete process.env.GIT_LITERAL_PATHSPECS

  try {
    const ctx = await runAll(options, logger)
    debugLog('Tasks were executed successfully!')
    printTaskOutput(ctx, logger)
    return true
  } catch (runAllError) {
    if (runAllError?.ctx?.errors) {
      const { ctx } = runAllError

      if (ctx.errors.has(ConfigNotFoundError)) {
        logger.error(NO_CONFIGURATION)
      } else if (ctx.errors.has(ApplyEmptyCommitError)) {
        logger.warn(PREVENTED_EMPTY_COMMIT)
      } else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
        logger.error(GIT_ERROR)
        if (ctx.shouldBackup) {
          // No sense to show this if the backup stash itself is missing.
          logger.error(RESTORE_STASH_EXAMPLE)
        }
      }

      printTaskOutput(ctx, logger)
      return false
    }

    // Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
    throw runAllError
  }
}

export default lintStaged


code:
/** @typedef {import('./index').Logger} Logger */

import fs from 'node:fs/promises'
import path from 'node:path'

import debug from 'debug'
import YAML from 'yaml'

import {
  CONFIG_FILE_NAMES,
  CONFIG_NAME,
  PACKAGE_JSON_FILE,
  PACKAGE_YAML_FILES,
} from './configFiles.js'
import { dynamicImport } from './dynamicImport.js'
import { failedToLoadConfig } from './messages.js'
import { resolveConfig } from './resolveConfig.js'

const debugLog = debug('lint-staged:loadConfig')

const jsonParse = (filePath, content) => {
  const isPackageFile = PACKAGE_JSON_FILE.includes(path.basename(filePath))
  try {
    const json = JSON.parse(content)
    return isPackageFile ? json[CONFIG_NAME] : json
  } catch (error) {
    if (path.basename(filePath) === PACKAGE_JSON_FILE) {
      debugLog('Ignoring invalid package file `%s` with content:\n%s', filePath, content)
      return undefined
    }

    throw error
  }
}

const yamlParse = (filePath, content) => {
  const isPackageFile = PACKAGE_YAML_FILES.includes(path.basename(filePath))
  try {
    const yaml = YAML.parse(content)
    return isPackageFile ? yaml[CONFIG_NAME] : yaml
  } catch (error) {
    if (isPackageFile) {
      debugLog('Ignoring invalid package file `%s` with content:\n%s', filePath, content)
      return undefined
    }

    throw error
  }
}

const NO_EXT = 'noExt'

/**
 * `lilconfig` doesn't support yaml files by default,
 * so we add custom loaders for those. Files without
 * an extensions are assumed to be yaml — this
 * assumption is in `cosmiconfig` as well.
 */
const loaders = {
  '.js': dynamicImport,
  '.json': jsonParse,
  '.mjs': dynamicImport,
  '.cjs': dynamicImport,
  '.yaml': yamlParse,
  '.yml': yamlParse,
  [NO_EXT]: yamlParse,
}

const readFile = async (filepath) => {
  const absolutePath = path.resolve(filepath)
  const file = await fs.readFile(absolutePath)
  return await file.toString()
}

const loadConfigByExt = async (filepath) => {
  filepath = path.resolve(filepath)
  const ext = path.extname(filepath) || NO_EXT
  const loader = loaders[ext]

  /**
   * No need to read file contents when loader only takes in the filepath argument
   * and reads itself; this is for `lilconfig` compatibility
   */
  const content = loader.length > 1 ? await readFile(filepath) : undefined

  return {
    config: await loader(filepath, content),
    filepath,
  }
}

/**
 * @param {object} options
 * @param {string} [options.configPath] - Explicit path to a config file
 * @param {string} [options.cwd] - Current working directory
 */
export const loadConfig = async ({ configPath, cwd }, logger) => {
  try {
    let result

    if (configPath) {
      debugLog('Loading configuration from `%s`...', configPath)
      result = await loadConfigByExt(resolveConfig(configPath))
    } else {
      debugLog('Searching for configuration from `%s`...', cwd)
      const { lilconfig } = await import('lilconfig')
      const explorer = lilconfig(CONFIG_NAME, { searchPlaces: CONFIG_FILE_NAMES, loaders })
      result = await explorer.search(cwd)
    }

    if (!result) return {}

    // config is a promise when using the `dynamicImport` loader
    const config = (await result.config) ?? null
    const filepath = result.filepath

    debugLog('Successfully loaded config from `%s`:\n%O', filepath, config)

    return { config, filepath }
  } catch (error) {
    debugLog('Failed to load configuration from `%s` with error:\n', configPath, error)
    logger.error(failedToLoadConfig(configPath))
    return {}
  }
}


code:
import debug from 'debug'

import { configurationError } from './messages.js'
import { resolveTaskFn } from './resolveTaskFn.js'

const debugLog = debug('lint-staged:makeCmdTasks')

/**
 * Creates and returns an array of listr tasks which map to the given commands.
 *
 * @param {object} options
 * @param {Array<string|Function>|string|Function} options.commands
 * @param {string} options.cwd
 * @param {Array<string>} options.files
 * @param {string} options.topLevelDir
 * @param {Boolean} shell
 * @param {Boolean} verbose
 */
export const makeCmdTasks = async ({ commands, cwd, files, topLevelDir, shell, verbose }) => {
  debugLog('Creating listr tasks for commands %o', commands)
  const commandArray = Array.isArray(commands) ? commands : [commands]
  const cmdTasks = []

  for (const cmd of commandArray) {
    // command function may return array of commands that already include `stagedFiles`
    const isFn = typeof cmd === 'function'

    /** Pass copy of file list to prevent mutation by function from config file. */
    const resolved = isFn ? await cmd([...files]) : cmd

    const resolvedArray = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array

    for (const command of resolvedArray) {
      // If the function linter didn't return string | string[]  it won't work
      // Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
      if (isFn && typeof command !== 'string') {
        throw new Error(
          configurationError(
            '[Function]',
            'Function task should return a string or an array of strings',
            resolved
          )
        )
      }

      const task = resolveTaskFn({ command, cwd, files, topLevelDir, isFn, shell, verbose })
      cmdTasks.push({ title: command, command, task })
    }
  }

  return cmdTasks
}


code:
import { inspect } from 'node:util'

import chalk from 'chalk'

import { error, info, warning } from './figures.js'

export const configurationError = (opt, helpMsg, value) =>
  `${chalk.redBright(`${error} Validation Error:`)}

  Invalid value for '${chalk.bold(opt)}': ${chalk.bold(inspect(value))}

  ${helpMsg}`

export const NOT_GIT_REPO = chalk.redBright(`${error} Current directory is not a git directory!`)

export const FAILED_GET_STAGED_FILES = chalk.redBright(`${error} Failed to get staged files!`)

export const incorrectBraces = (before, after) =>
  chalk.yellow(
    `${warning} Detected incorrect braces with only single value: \`${before}\`. Reformatted as: \`${after}\`
`
  )

export const NO_CONFIGURATION = `${error} No valid configuration found.`

export const NO_STAGED_FILES = `${info} No staged files found.`

export const NO_TASKS = `${info} No staged files match any configured task.`

export const skippingBackup = (hasInitialCommit, diff) => {
  const reason =
    diff !== undefined
      ? '`--diff` was used'
      : hasInitialCommit
        ? '`--no-stash` was used'
        : 'there’s no initial commit yet'

  return chalk.yellow(`${warning} Skipping backup because ${reason}.\n`)
}

export const skippingHidePartiallyStaged = (stash, diff) => {
  const reason =
    diff !== undefined
      ? '`--diff` was used'
      : !stash
        ? '`--no-stash` was used'
        : '`--no-hide-partially-staged` was used'

  return chalk.yellow(
    `${warning} Skipping hiding unstaged changes from partially staged files because ${reason}.\n`
  )
}

export const DEPRECATED_GIT_ADD = chalk.yellow(
  `${warning} 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.
`
)

export const TASK_ERROR = 'Skipped because of errors from tasks.'

export const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.'

export const GIT_ERROR = `\n  ${chalk.redBright(`${error} lint-staged failed due to a git error.`)}`

export const invalidOption = (name, value, message) => `${chalk.redBright(
  `${error} Validation Error:`
)}

  Invalid value for option '${chalk.bold(name)}': ${chalk.bold(value)}

  ${message}

See https://github.com/okonet/lint-staged#command-line-flags`

export const PREVENTED_EMPTY_COMMIT = `
  ${chalk.yellow(`${warning} lint-staged prevented an empty git commit.
  Use the --allow-empty option to continue, or check your task configuration`)}
`

export const RESTORE_STASH_EXAMPLE = `  Any lost modifications can be restored from a git stash:

    > git stash list
    stash@{0}: automatic lint-staged backup
    > git stash apply --index stash@{0}
`

export const CONFIG_STDIN_ERROR = chalk.redBright(`${error} Failed to read config from stdin.`)

export const failedToLoadConfig = (filepath) =>
  chalk.redBright(`${error} Failed to read config from file "${filepath}".`)

export const failedToParseConfig = (
  filepath,
  error
) => `${chalk.redBright(`${error} Failed to parse config from file "${filepath}".`)}

${error}

See https://github.com/okonet/lint-staged#configuration.`


code:
/**
 * Reimplementation of "normalize-path"
 * @see https://github.com/jonschlinkert/normalize-path/blob/52c3a95ebebc2d98c1ad7606cbafa7e658656899/index.js
 */

/*!
 * normalize-path <https://github.com/jonschlinkert/normalize-path>
 *
 * Copyright (c) 2014-2018, Jon Schlinkert.
 * Released under the MIT License.
 */

import path from 'node:path'

/**
 * A file starting with \\?\
 * @see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
 */
const WIN32_FILE_NS = '\\\\?\\'

/**
 * A file starting with \\.\
 * @see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
 */
const WIN32_DEVICE_NS = '\\\\.\\'

/**
 * Normalize input file path to use POSIX separators
 * @param {String} input
 * @returns String
 */
export const normalizePath = (input) => {
  if (input === path.posix.sep || input === path.win32.sep) {
    return path.posix.sep
  }

  let normalized = input.split(/[/\\]+/).join(path.posix.sep)

  /** Handle win32 Namespaced paths by changing e.g. \\.\ to //./ */
  if (input.startsWith(WIN32_FILE_NS) || input.startsWith(WIN32_DEVICE_NS)) {
    normalized = normalized.replace(/^\/(\.|\?)/, '//$1')
  }

  /** Remove trailing slash */
  if (normalized.endsWith(path.posix.sep)) {
    normalized = normalized.slice(0, -1)
  }

  return normalized
}


code:
/**
 * Return array of strings split from the output of `git <something> -z`.
 * With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to
 * remove the last occurrence of `\u0000` before splitting
 */
export const parseGitZOutput = (input) =>
  input
    ? input
        .replace(/\u0000$/, '') // eslint-disable-line no-control-regex
        .split('\u0000')
    : []


code:
/**
 * Handle logging of listr `ctx.output` to the specified `logger`
 * @param {Object} ctx - The listr initial state
 * @param {Object} logger - The logger
 */
export const printTaskOutput = (ctx = {}, logger) => {
  if (!Array.isArray(ctx.output)) return
  const log = ctx.errors?.size > 0 ? logger.error : logger.log
  for (const line of ctx.output) {
    log(line)
  }
}


code:
import { createInterface } from 'node:readline'

/**
 * Returns a promise resolving to the first line written to stdin after invoking.
 * @warn will never resolve if called after writing to stdin
 *
 * @returns {Promise<string>}
 */
export const readStdin = () => {
  const readline = createInterface({ input: process.stdin })

  return new Promise((resolve) => {
    readline.prompt()
    readline.on('line', (line) => {
      readline.close()
      resolve(line)
    })
  })
}


code:
import { createRequire } from 'node:module'

/**
 * require() does not exist for ESM, so we must create it to use require.resolve().
 * @see https://nodejs.org/api/module.html#modulecreaterequirefilename
 */
const require = createRequire(import.meta.url)

export function resolveConfig(configPath) {
  try {
    return require.resolve(configPath)
  } catch {
    return configPath
  }
}


code:
import path from 'node:path'

import debug from 'debug'

import { execGit } from './execGit.js'
import { normalizePath } from './normalizePath.js'

const debugLog = debug('lint-staged:resolveGitRepo')

/**
 * Resolve .git directory relative to repo top-level directory
 *
 * @example ".git"
 */
const resolveRelativeGitDir = async (cwd) => {
  /**
   * Absolute repo top-level directory
   *
   * @example
Git on macOS

   * "/Users/iiro/Documents/git/lint-staged"
   *
   * @example
Git for Windows

   * "C:\Users\iiro\Documents\git\lint-staged"
   *
   * @example
Git installed with MSYS2, this doesn't work when used as CWD with Node.js child_process

   * "/c/Users/iiro/Documents/git/lint-staged"
   */
  const topLevelPromise = execGit(['rev-parse', '--show-toplevel'], { cwd })

  /**
   * Absolute .git directory, similar to top-level
   *
   * @example "/Users/iiro/Documents/git/lint-staged/.git"
   */
  const absoluteGitDirPromise = execGit(['rev-parse', '--absolute-git-dir'], { cwd })

  const [topLevel, absoluteGitDir] = await Promise.all([topLevelPromise, absoluteGitDirPromise])

  return path.relative(topLevel, absoluteGitDir)
}

/** Resolve git directory and possible submodule paths */
export const resolveGitRepo = async (cwd = process.cwd()) => {
  try {
    debugLog('Resolving git repo from `%s`', cwd)

    // Unset GIT_DIR before running any git operations in case it's pointing to an incorrect location
    debugLog('Unset GIT_DIR (was `%s`)', process.env.GIT_DIR)
    delete process.env.GIT_DIR
    debugLog('Unset GIT_WORK_TREE (was `%s`)', process.env.GIT_WORK_TREE)
    delete process.env.GIT_WORK_TREE

    const relativeTopLevelDir = await execGit(['rev-parse', '--show-cdup'], { cwd })
    const topLevelDir = normalizePath(path.join(cwd, relativeTopLevelDir))
    debugLog('Resolved git repository top-level directory to be `%s`', topLevelDir)

    const relativeGitConfigDir = await resolveRelativeGitDir(cwd)
    const gitConfigDir = normalizePath(path.join(topLevelDir, relativeGitConfigDir))
    debugLog('Resolved git config directory to be `%s`', gitConfigDir)

    return { topLevelDir, gitConfigDir }
  } catch (error) {
    debugLog('Failed to resolve git repo with error:', error)
    return { error, topLevelDir: null, gitConfigDir: null }
  }
}


code:
import chalk from 'chalk'
import debug from 'debug'
import { execa, execaCommand } from 'execa'
import pidTree from 'pidtree'
import { parseArgsStringToArgv } from 'string-argv'

import { error, info } from './figures.js'
import { getInitialState } from './state.js'
import { TaskError } from './symbols.js'

const TASK_ERROR = 'lint-staged:taskError'

const debugLog = debug('lint-staged:resolveTaskFn')

const getTag = ({ code, killed, signal }) => (killed && 'KILLED') || signal || code || 'FAILED'

/**
 * Handle task console output.
 *
 * @param {string} command
 * @param {Object} result
 * @param {string} result.stdout
 * @param {string} result.stderr
 * @param {boolean} result.failed
 * @param {boolean} result.killed
 * @param {string} result.signal
 * @param {Object} ctx
 * @returns {Error}
 */
const handleOutput = (command, result, ctx, isError = false) => {
  const { stderr, stdout } = result
  const hasOutput = !!stderr || !!stdout

  if (hasOutput) {
    const outputTitle = isError ? chalk.redBright(`${error} ${command}:`) : `${info} ${command}:`
    const output = []
      .concat(ctx.quiet ? [] : ['', outputTitle])
      .concat(stderr ? stderr : [])
      .concat(stdout ? stdout : [])
    ctx.output.push(output.join('\n'))
  } else if (isError) {
    // Show generic error when task had no output
    const tag = getTag(result)
    const message = chalk.redBright(`\n${error} ${command} failed without output (${tag}).`)
    if (!ctx.quiet) ctx.output.push(message)
  }
}

/**
 * Kill an execa process along with all its child processes.
 * @param {execa.ExecaChildProcess<string>} execaProcess
 */
const killExecaProcess = async (execaProcess) => {
  try {
    const childPids = await pidTree(execaProcess.pid)
    for (const childPid of childPids) {
      try {
        process.kill(childPid)
      } catch (error) {
        debugLog(`Failed to kill process with pid "%d": %o`, childPid, error)
      }
    }
  } catch (error) {
    // Suppress "No matching pid found" error. This probably means
    // the process already died before executing.
    debugLog(`Failed to kill process with pid "%d": %o`, execaProcess.pid, error)
  }

  // The execa process is killed separately in order to get the `KILLED` status.
  execaProcess.kill()
}

/**
 * Interrupts the execution of the execa process that we spawned if
 * another task adds an error to the context.
 *
 * @param {Object} ctx
 * @param {execa.ExecaChildProcess<string>} execaChildProcess
 * @returns {() => Promise<void>} Function that clears the interval that
 * checks the context.
 */
const interruptExecutionOnError = (ctx, execaChildProcess) => {
  let killPromise

  const errorListener = async () => {
    killPromise = killExecaProcess(execaChildProcess)
    await killPromise
  }

  ctx.events.on(TASK_ERROR, errorListener, { once: true })

  return async () => {
    ctx.events.off(TASK_ERROR, errorListener)
    await killPromise
  }
}

/**
 * Create a error output depending on process result.
 *
 * @param {string} command
 * @param {Object} result
 * @param {string} result.stdout
 * @param {string} result.stderr
 * @param {boolean} result.failed
 * @param {boolean} result.killed
 * @param {string} result.signal
 * @param {Object} ctx
 * @returns {Error}
 */
const makeErr = (command, result, ctx) => {
  ctx.errors.add(TaskError)

  // https://nodejs.org/api/events.html#error-events
  ctx.events.emit(TASK_ERROR, TaskError)

  handleOutput(command, result, ctx, true)
  const tag = getTag(result)
  return new Error(`${chalk.redBright(command)} ${chalk.dim(`[${tag}]`)}`)
}

/**
 * Returns the task function for the linter.
 *
 * @param {Object} options
 * @param {string} options.command — Linter task
 * @param {string} [options.cwd]
 * @param {String} options.topLevelDir - Current git repo top-level path
 * @param {Boolean} options.isFn - Whether the linter task is a function
 * @param {Array<string>} options.files — Filepaths to run the linter task against
 * @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support
 * @param {Boolean} [options.verbose] — Always show task verbose
 * @returns {() => Promise<Array<string>>}
 */
export const resolveTaskFn = ({
  command,
  cwd = process.cwd(),
  files,
  topLevelDir,
  isFn,
  shell = false,
  verbose = false,
}) => {
  const [cmd, ...args] = parseArgsStringToArgv(command)
  debugLog('cmd:', cmd)
  debugLog('args:', args)

  const execaOptions = {
    // Only use topLevelDir as CWD if we are using the git binary
    // e.g `npm` should run tasks in the actual CWD
    cwd: /^git(\.exe)?/i.test(cmd) ? topLevelDir : cwd,
    preferLocal: true,
    reject: false,
    shell,
    stdin: 'ignore',
  }

  debugLog('execaOptions:', execaOptions)

  return async (ctx = getInitialState()) => {
    const execaChildProcess = shell
      ? execaCommand(isFn ? command : `${command} ${files.join(' ')}`, execaOptions)
      : execa(cmd, isFn ? args : args.concat(files), execaOptions)

    const quitInterruptCheck = interruptExecutionOnError(ctx, execaChildProcess)
    const result = await execaChildProcess
    await quitInterruptCheck()

    if (result.failed || result.killed || result.signal != null) {
      throw makeErr(command, result, ctx)
    }

    if (verbose) {
      handleOutput(command, result, ctx)
    }
  }
}