Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use git stashes in git workflow for better performance #724

Merged
merged 71 commits into from
Jan 19, 2020
Merged
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
40a5db1
feat: use git stashes for gitWorkflow
iiroj Jul 21, 2019
5be9cb4
ci: print git version number in AppVeyor
iiroj Jul 21, 2019
762d4d1
refactor: simplify gitWorkflow by creating only one stash
iiroj Jul 21, 2019
d843d07
test: update test for restoring changes
iiroj Jul 21, 2019
76cb08f
fix: retry failing apply with 3-way merge
iiroj Jul 21, 2019
9b101cd
refactor: improvements
iiroj Jul 22, 2019
128631d
refactor: GitWorkflow is a class
iiroj Jul 22, 2019
061946c
test: increase jest timeout for slow CI runners
iiroj Jul 23, 2019
357934f
fix: try applying unstaged changes before handling errors
iiroj Aug 17, 2019
bbfdbce
test: add some runAll tests
iiroj Aug 17, 2019
a2b9716
test: add test for failing merge conflict resolution
iiroj Sep 10, 2019
effdb4c
refactor: use execa.command to avoid parsing of commands
iiroj Sep 11, 2019
2f1e886
fix: gitWorkflow handles active merge mode
iiroj Sep 11, 2019
e68255a
test: move mock files inside test directory
iiroj Sep 12, 2019
cb9aaf0
test: rework usage of tmp module
iiroj Sep 12, 2019
b3eb3fc
refactor: no need to use path.resolve since path is normalized
iiroj Sep 12, 2019
eb54a89
refactor: check for merge using fs.access before fs.readfile
iiroj Sep 13, 2019
158c900
refactor: move file operations to separate file
iiroj Sep 13, 2019
09e6edf
test: scope global jest timeout to only runAll.unmocked.spec
iiroj Sep 13, 2019
1b9e85a
test: remove non-functioning, throwing tests
iiroj Sep 13, 2019
49659ef
refactor: rename src/ to lib/ since it's not compiled
iiroj Sep 13, 2019
2b47643
refactor: improve long argument warning indentation
iiroj Sep 13, 2019
74fe356
refactor: show helpful warning about git failures
iiroj Sep 13, 2019
761f984
test: do not use tmp for creation of tmp directories
iiroj Sep 13, 2019
d8f7f1d
test: add mock console
iiroj Sep 13, 2019
90da8a8
test: further increase timeout for long test
iiroj Sep 13, 2019
fc03fdc
fix: keep untracked files around by backing them up
iiroj Sep 15, 2019
6fa9c85
refactor: improvements based on PR review
iiroj Sep 26, 2019
9c33515
docs: remove bit about next release
iiroj Oct 2, 2019
1a87333
fix: workaround for stashing deleted files for git < 2.23
iiroj Oct 2, 2019
91ee67a
test: should work when amending previous commit with unstaged changes
iiroj Oct 3, 2019
7b144b4
test: improve coverage of gitWorkflow
iiroj Oct 3, 2019
74ed28d
feat: automatically stage task modifications
iiroj Oct 21, 2019
5208399
feat: warn when task contains "git add"
iiroj Oct 23, 2019
d1da3fc
test: add test for supplying object config via node api
iiroj Oct 23, 2019
c7d0592
fix: correctly restore untracked files from backup stash
iiroj Oct 29, 2019
cce9809
fix: prevent Listr from hiding git add warning
iiroj Oct 30, 2019
6467a66
fix: update warning about git add, and to README
iiroj Nov 3, 2019
bc17363
test: should not resurrect removed files due to git bug
iiroj Nov 3, 2019
869bac6
fix: no need to run `git clean -df` since untracked changes are stashed
iiroj Nov 3, 2019
bf532c2
fix: add all modified files to git index with `git add .`
iiroj Nov 14, 2019
e58ebbf
Revert "fix: no need to run `git clean -df` since untracked changes a…
iiroj Nov 14, 2019
cfde9ca
fix: correctly leave only staged files for running tasks
iiroj Nov 14, 2019
7b3a334
fix: support binary files
iiroj Nov 14, 2019
a33dc68
Merge branch 'master' into beta
iiroj Nov 14, 2019
5d989ac
chore: update all dependencies
iiroj Nov 15, 2019
5f0a807
chore(deps): remove unused devDependencies
iiroj Nov 17, 2019
6b66cf3
Merge branch 'refs/heads/master' into beta
iiroj Nov 19, 2019
cb43872
feat: split tasks into chunks to support shells with limited max argu…
iiroj Nov 20, 2019
f88e226
fix: improve debug logging
iiroj Nov 27, 2019
03ea132
refactor: linStaged is an async function
iiroj Nov 27, 2019
80406c2
fix: max arg length is by default half of the allowed to prevent edge…
iiroj Nov 27, 2019
4bf94e2
Merge branch 'refs/heads/master' into beta
iiroj Nov 27, 2019
0eedacd
test: remove non-working concurrency tests for now
iiroj Nov 27, 2019
814b9df
feat: bump Node.js version dependency to at least 10.13.0 (#747)
iiroj Dec 3, 2019
083b8e7
fix: automatically add modifications only to originally staged files
iiroj Dec 4, 2019
f3ae378
fix: better workaround for git stash --keep-index bug
iiroj Dec 14, 2019
22ba124
refactor: minor optimizations
iiroj Dec 14, 2019
f8ddfc2
fix: restore metadata about git merge before running tasks
iiroj Dec 16, 2019
d091f71
fix: correctly recover when unstaged changes cannot be restored
iiroj Dec 17, 2019
9913bb2
test: do not write file into repo during test run
iiroj Dec 17, 2019
1b64239
fix: fail with a message when backup stash is missing
iiroj Dec 17, 2019
f2a2702
Merge branch 'master' into beta
iiroj Dec 17, 2019
20d5c5d
feat: support async function tasks
iiroj Dec 18, 2019
da22cf2
fix: handle git MERGE_* files separately; improve error handling
iiroj Dec 20, 2019
30b4809
fix: error handling skips dropping backup stash after internal git er…
iiroj Dec 24, 2019
8bdeec0
feat: throw error to prevent empty commits unless --allow-empty is us…
iiroj Jan 8, 2020
bd3721f
Merge branch 'master' into beta
iiroj Jan 9, 2020
82bee06
Merge branch 'master' into beta
iiroj Jan 15, 2020
e1cd6ba
Merge branch 'master' into beta
iiroj Jan 16, 2020
f9e128d
docs: Improve config section documentation
okonet Jan 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

environment:
matrix:
- nodejs_version: '13'
- nodejs_version: '12'
- nodejs_version: '10'
- nodejs_version: '8'

matrix:
fast_finish: true
Expand All @@ -25,6 +25,7 @@ cache:
test_script:
- node --version
- yarn --version
- git --version
- yarn test

branches:
Expand Down
6 changes: 3 additions & 3 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"*.{js,json,md}": ["prettier --write", "git add"],
"*.js": ["npm run lint:base --fix", "git add"],
".*{rc, json}": ["jsonlint --in-place", "git add"]
"*.{js,json,md}": "prettier --write",
"*.js": "npm run lint:base --fix",
".*{rc, json}": "jsonlint --in-place"
}
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

language: node_js
node_js:
- '13'
- '12'
- '10'
- '8'

before_install: yarn global add greenkeeper-lockfile@1
install: yarn install
Expand Down
101 changes: 43 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,6 @@

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

---

## 🚧 Help test `lint-staged@beta`!

Version 10 of `lint-staged` is coming with changes that help it run faster on large git repositories and prevent loss of data during errors. Please help test the `beta` version and report any inconsistencies in our [GitHub Issues](https://github.com/okonet/lint-staged/issues):

**Using npm**

npm install --save-dev lint-staged@beta

**Using yarn**

yarn add -D lint-staged@beta

### Notable changes

- A git stash is created before running any tasks, so in case of errors any lost changes can be restored easily (and automatically unless lint-staged itself crashes)
- Instead of write-tree/read-tree, `lint-staged@beta` uses git stashes to hide unstaged changes while running tasks against staged files
- This results in a performance increase of up to 45x on very large repositories
- The behaviour of committing modifications during tasks (eg. `prettier --write && git add`) is different. The current version creates a diff of these modifications, and applies it against the original state, silently ignoring any errors. The `beta` version leaves modifications of staged files as-is, and then restores all hidden unstaged changes as patch. If applying the patch fails due to a merge conflict (because tasks have modified the same lines), a 3-way merge will be retried. If this also fails, the entire commit will fail and the original state will be restored.
- **TL;DR** the `beta` version will never skip committing any changes by tasks (due to a merge conflict), but might fail in very complex situations where unstaged changes cannot be restored cleanly. If this happens to you, we are very interested in a repeatable test scenario.

---

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

## Why
Expand Down Expand Up @@ -66,31 +42,34 @@ See [Releases](https://github.com/okonet/lint-staged/releases)
## Command line flags

```bash
$ npx lint-staged --help
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
-p, --concurrent [parallel tasks] The number of tasks to run concurrently, or false to run tasks sequentially
-h, --help output usage information
```

-V, --version output the version number
--allow-empty allow empty commits when tasks revert all staged changes (default: false)
-c, --config [path] path to configuration file
-d, --debug print additional debug information (default: false)
-p, --concurrent <parallel tasks> the number of tasks to run concurrently, or false to run tasks serially (default: true)
-q, --quiet disable lint-staged’s own console output (default: false)
-r, --relative pass relative filepaths to tasks (default: false)
-x, --shell skip parsing of tasks for better shell support (default: false)
-h, --help output usage information
```

- **`--allow-empty`**: By default lint-stage will exit with an error — aborting the commit — when after running tasks there are no staged modifications. Use this disable this behaviour and create empty git commits.
iiroj marked this conversation as resolved.
Show resolved Hide resolved
- This can happen when tasks use libraries like _prettier_ or _eslint_ with automatic code formatting
- **`--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.
- **`--relative`**: By default filepaths will be passed to the linter tasks as _absolute_. This flag makes them relative to `process.cwd()` (where `lint-staged` runs).
- **`--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.
- **`--quiet`**: By default `lint-staged` will print progress status to console while running linters. Use this flag to supress all output, except for linter scripts.
- **`--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`; this causes serial, uncoloured output to the terminal, instead of the default (beautified, dynamic) output.
- **`--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`.
- **`--quiet`**: By default `lint-staged` will print progress status to console while running linters. Use this flag to supress all output, except for linter scripts.
- **`--relative`**: By default filepaths will be passed to the linter tasks as _absolute_. This flag makes them relative to `process.cwd()` (where `lint-staged` runs).
- **`--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.

## Configuration

Expand Down Expand Up @@ -177,12 +156,12 @@ Pass arguments to your commands separated by space as you would do in the shell.

Starting from [v2.0.0](https://github.com/okonet/lint-staged/releases/tag/2.0.0) sequences of commands are supported. Pass an array of commands instead of a single one and they will run sequentially. This is useful for running autoformatting tools like `eslint --fix` or `stylefmt` but can be used for any arbitrary sequences.

## Using JS functions to customize linter commands
## Using JS functions to customize tasks

When supplying configuration in JS format it is possible to define the linter command as a function which receives an array of staged filenames/paths and returns the complete linter command as a string. It is also possible to return an array of complete command strings, for example when the linter command supports only a single file input.
When supplying configuration in JS format it is possible to define the task as a function, which will receive an array of staged filenames/paths and should return the complete command as a string. It is also possible to return an array of complete command strings, for example when the task supports only a single file input. The function can be either sync or async.

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

### Example: Wrap filenames in single quotes and run once per file
Expand Down Expand Up @@ -221,7 +200,7 @@ const micromatch = require('micromatch')
module.exports = {
'*': allFiles => {
const match = micromatch(allFiles, ['*.js', '*.ts'])
return `eslint ${match.join(" ")}`
return `eslint ${match.join(' ')}`
}
}
```
Expand All @@ -238,7 +217,7 @@ module.exports = {
'*.js': files => {
// from `files` filter those _NOT_ matching `*test.js`
const match = micromatch.not(files, '*test.js')
return `eslint ${match.join(" ")}`
return `eslint ${match.join(' ')}`
}
}
```
Expand All @@ -261,15 +240,15 @@ module.exports = {

## 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`. After the code is reformatted, we want it to be added to the same commit. This can be done using following config:
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", "git add"]
"*.js": "prettier --write"
}
```

Starting from v8, lint-staged will stash your remaining changes (not added to the index) and restore them from stash afterwards if there are partially staged files detected. This allows you to create partial commits with hunks using `git add --patch`. See the [blog post](https://medium.com/@okonetchnikov/announcing-lint-staged-with-support-for-partially-staged-files-abc24a40d3ff)
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

Expand Down Expand Up @@ -305,7 +284,7 @@ _Note we don’t pass a path as an argument for the runners. This is important s

```json
{
"*.js": ["eslint --fix", "git add"]
"*.js": "eslint --fix"
}
```

Expand All @@ -317,15 +296,15 @@ If you wish to reuse a npm script defined in your package.json:

```json
{
"*.js": ["npm run my-custom-script --", "git add"]
"*.js": "npm run my-custom-script --"
}
```

The following is equivalent:

```json
{
"*.js": ["linter --arg1 --arg2", "git add"]
"*.js": "linter --arg1 --arg2"
}
```

Expand All @@ -345,19 +324,19 @@ For example, here is `jest` running on all `.js` files with the `NODE_ENV` varia

```json
{
"*.{js,jsx}": ["prettier --write", "git add"]
"*.{js,jsx}": "prettier --write"
}
```

```json
{
"*.{ts,tsx}": ["prettier --write", "git add"]
"*.{ts,tsx}": "prettier --write"
}
```

```json
{
"*.{md,html}": ["prettier --write", "git add"]
"*.{md,html}": "prettier --write"
}
```

Expand All @@ -370,19 +349,19 @@ For example, here is `jest` running on all `.js` files with the `NODE_ENV` varia
}
```

### Run PostCSS sorting, add files to commit and run Stylelint to check
### Run PostCSS sorting and Stylelint to check

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

### Minify the images and add files to commit
### Minify the images

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

Expand All @@ -399,7 +378,7 @@ See more on [this blog post](https://medium.com/@tomchentw/imagemin-lint-staged-

```json
{
"*.{js,jsx}": ["flow focus-check", "git add"]
"*.{js,jsx}": "flow focus-check"
}
```

Expand All @@ -426,6 +405,8 @@ Parameters to `lintStaged` are equivalent to their CLI counterparts:
```js
const success = await lintStaged({
configPath: './path/to/configuration/file',
maxArgLength: null,
relative: false,
shell: false,
quiet: false,
debug: false
Expand All @@ -439,12 +420,16 @@ const success = await lintStaged({
config: {
'*.js': 'eslint --fix'
},
maxArgLength: null,
relative: false,
shell: false,
quiet: false,
debug: 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.

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

_**Update**_: The latest version of JetBrains IDEs now support running hooks as you would expect.
Expand Down Expand Up @@ -503,7 +488,7 @@ Example repo: [sudo-suhas/lint-staged-django-react-demo](https://github.com/sudo

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

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 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 ).

Based on the discussion from 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.

Expand Down
53 changes: 39 additions & 14 deletions bin/lint-staged
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,30 @@ const pkg = require('../package.json')
require('please-upgrade-node')(
Object.assign({}, pkg, {
engines: {
node: '>=8.12.0'
node: '>=10.13.0' // First LTS release of 'Dubnium'
}
})
)

const cmdline = require('commander')
const debugLib = require('debug')
const lintStaged = require('../src')
const lintStaged = require('../lib')

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

cmdline
.version(pkg.version)
.option('-c, --config [path]', 'Path to configuration file')
.option('-r, --relative', 'Pass relative filepaths to tasks')
.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('--allow-empty', 'allow empty commits when tasks revert all staged changes', false)
.option('-c, --config [path]', 'path to configuration file')
.option('-d, --debug', 'print additional debug information', false)
.option(
'-p, --concurrent <parallel tasks>',
'The number of tasks to run concurrently, or false to run tasks serially',
'the number of tasks to run concurrently, or false to run tasks serially',
true
)
)
.option('-q, --quiet', 'disable lint-staged’s own console output', false)
.option('-r, --relative', 'pass relative filepaths to tasks', false)
.option('-x, --shell', 'skip parsing of tasks for better shell support', false)
.parse(process.argv)

if (cmdline.debug) {
Expand All @@ -47,14 +48,38 @@ if (cmdline.debug) {

debug('Running `lint-staged@%s`', pkg.version)

lintStaged({
/**
* 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
}
}

const options = {
allowEmpty: !!cmdline.allowEmpty,
concurrent: cmdline.concurrent,
configPath: cmdline.config,
debug: !!cmdline.debug,
maxArgLength: getMaxArgLength() / 2,
quiet: !!cmdline.quiet,
relative: !!cmdline.relative,
shell: !!cmdline.shell,
quiet: !!cmdline.quiet,
debug: !!cmdline.debug,
concurrent: cmdline.concurrent
})
}

debug('Options parsed from command-line:', options)

lintStaged(options)
.then(passed => {
process.exitCode = passed ? 0 : 1
})
Expand Down