Skip to content

Commit

Permalink
feat: add --no-stash option to disable the backup stash, and not re…
Browse files Browse the repository at this point in the history
…vert in case of errors
  • Loading branch information
iiroj committed Mar 30, 2020
1 parent 7f2ef33 commit c386e4c
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 60 deletions.
14 changes: 10 additions & 4 deletions README.md
Expand Up @@ -60,13 +60,18 @@ Usage: lint-staged [options]

Options:
-V, --version output the version number
--allow-empty allow empty commits when tasks undo all staged changes (default: false)
--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)
--no-stash disable the backup stash, and do not revert in case of
errors
-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)
-x, --shell skip parsing of tasks for better shell support (default:
false)
-h, --help output usage information
```
Expand All @@ -79,6 +84,7 @@ Options:
- `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`.
- **`--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.
- **`--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.
Expand Down Expand Up @@ -168,7 +174,7 @@ Pass arguments to your commands separated by space as you would do in the shell.
## 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.
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:
Expand Down
2 changes: 2 additions & 0 deletions bin/lint-staged.js
Expand Up @@ -31,6 +31,7 @@ cmdline
.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('--no-stash', 'disable the backup stash, and do not revert in case of errors', false)
.option(
'-p, --concurrent <parallel tasks>',
'the number of tasks to run concurrently, or false to run tasks serially',
Expand Down Expand Up @@ -71,6 +72,7 @@ const options = {
configPath: cmdline.config,
debug: !!cmdline.debug,
maxArgLength: getMaxArgLength() / 2,
stash: !!cmdline.stash, // commander inverts `no-<x>` flags to `!x`
quiet: !!cmdline.quiet,
relative: !!cmdline.relative,
shell: !!cmdline.shell
Expand Down
17 changes: 10 additions & 7 deletions lib/gitWorkflow.js
Expand Up @@ -54,6 +54,7 @@ const handleError = (error, ctx) => {
class GitWorkflow {
constructor({ allowEmpty, gitConfigDir, gitDir, stagedFileChunks }) {
this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: gitDir })
this.deletedFiles = []
this.gitConfigDir = gitConfigDir
this.gitDir = gitDir
this.unstagedDiff = null
Expand Down Expand Up @@ -161,10 +162,9 @@ class GitWorkflow {
}

/**
* Create backup stashes, one of everything and one of only staged changes
* Staged files are left in the index for running tasks
* Create a diff of partially staged files and backup stash if enabled.
*/
async createBackup(ctx) {
async prepare(ctx, stash) {
try {
debug('Backing up original state...')

Expand All @@ -179,6 +179,11 @@ class GitWorkflow {
await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
}

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

// Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
// - in git versions =< 2.13.0 the `--keep-index` flag resurrects deleted files
// - git stash can't infer RD or MD states correctly, and will lose the deletion
Expand Down Expand Up @@ -291,9 +296,7 @@ class GitWorkflow {
await Promise.all(this.deletedFiles.map(file => unlink(file)))

// Clean out patch
if (this.partiallyStagedFiles) {
await unlink(PATCH_UNSTAGED)
}
if (this.partiallyStagedFiles) await unlink(PATCH_UNSTAGED)

debug('Done restoring original state!')
} catch (error) {
Expand All @@ -305,7 +308,7 @@ class GitWorkflow {
/**
* Drop the created stashes after everything has run
*/
async dropBackup(ctx) {
async cleanup(ctx) {
try {
debug('Dropping backup stash...')
await this.execGit(['stash', 'drop', '--quiet', await this.getBackupStash(ctx)])
Expand Down
13 changes: 7 additions & 6 deletions lib/index.js
Expand Up @@ -46,12 +46,12 @@ function loadConfig(configPath) {
* @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 {boolean} [options.debug] - Enable debug mode
* @param {number} [options.maxArgLength] - Maximum argument string length
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
* @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
* @param {boolean} [options.debug] - Enable debug mode
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
* @param {Logger} [logger]
*
* @returns {Promise<boolean>} Promise of whether the linting passed or failed
Expand All @@ -62,11 +62,12 @@ module.exports = async function lintStaged(
concurrent = true,
config: configObject,
configPath,
debug = false,
maxArgLength,
quiet = false,
relative = false,
shell = false,
quiet = false,
debug = false
stash = true
} = {},
logger = console
) {
Expand Down Expand Up @@ -98,7 +99,7 @@ module.exports = async function lintStaged(

try {
await runAll(
{ allowEmpty, concurrent, config, debug, maxArgLength, quiet, relative, shell },
{ allowEmpty, concurrent, config, debug, maxArgLength, stash, quiet, relative, shell },
logger
)
debugLog('tasks were executed successfully!')
Expand Down
33 changes: 22 additions & 11 deletions lib/runAll.js
Expand Up @@ -62,33 +62,41 @@ const shouldSkipCleanup = ctx => {
*
* @param {object} options
* @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
* @param {Object} [options.config] - Task configuration
* @param {Object} [options.cwd] - Current working directory
* @param {boolean} [options.debug] - Enable debug mode
* @param {number} [options.maxArgLength] - Maximum argument string length
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
* @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
* @param {boolean} [options.debug] - Enable debug mode
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
* @param {Logger} logger
* @returns {Promise}
*/
const runAll = async (
{
allowEmpty = false,
concurrent = true,
config,
cwd = process.cwd(),
debug = false,
maxArgLength,
quiet = false,
relative = false,
shell = false,
concurrent = true
stash = true
},
logger = console
) => {
debugLog('Running all linter scripts')

if (!stash) {
logger.warn(
`${symbols.warning} ${chalk.yellow('Skipping backup because `--no-stash` was used.')}`
)
}

const { gitDir, gitConfigDir } = await resolveGitRepo(cwd)
if (!gitDir) throw new Error('Current directory is not a git directory!')

Expand Down Expand Up @@ -188,8 +196,8 @@ const runAll = async (
const runner = new Listr(
[
{
title: 'Creating backup...',
task: ctx => git.createBackup(ctx)
title: 'Preparing...',
task: ctx => git.prepare(ctx, stash)
},
{
title: 'Hiding unstaged changes to partially staged files...',
Expand All @@ -200,7 +208,8 @@ const runAll = async (
{
title: 'Applying modifications...',
task: ctx => git.applyModifications(ctx),
skip: shouldSkipApplyModifications
// Always apply back unstaged modifications when skipping backup
skip: ctx => stash && shouldSkipApplyModifications(ctx)
},
{
title: 'Restoring unstaged changes to partially staged files...',
Expand All @@ -212,12 +221,14 @@ const runAll = async (
title: 'Reverting to original state because of errors...',
task: ctx => git.restoreOriginalState(ctx),
enabled: ctx =>
ctx.taskError || ctx.gitApplyEmptyCommitError || ctx.gitRestoreUnstagedChangesError,
stash &&
(ctx.taskError || ctx.gitApplyEmptyCommitError || ctx.gitRestoreUnstagedChangesError),
skip: shouldSkipRevert
},
{
title: 'Cleaning up backup...',
task: ctx => git.dropBackup(ctx),
title: 'Cleaning up...',
task: ctx => git.cleanup(ctx),
enabled: () => stash,
skip: shouldSkipCleanup
}
],
Expand All @@ -240,7 +251,7 @@ const runAll = async (
`\n The initial commit is needed for lint-staged to work.
Please use the --no-verify flag to skip running lint-staged.`
)
} else {
} else if (stash) {
// No sense to show this if the backup stash itself is missing.
logger.error(` Any lost modifications can be restored from a git stash:
Expand Down
6 changes: 4 additions & 2 deletions test/__mocks__/gitWorkflow.js
@@ -1,8 +1,10 @@
const stub = {
stashBackup: jest.fn().mockImplementation(() => Promise.resolve()),
prepare: jest.fn().mockImplementation(() => Promise.resolve()),
hideUnstagedChanges: jest.fn().mockImplementation(() => Promise.resolve()),
applyModifications: jest.fn().mockImplementation(() => Promise.resolve()),
restoreUnstagedChanges: jest.fn().mockImplementation(() => Promise.resolve()),
restoreOriginalState: jest.fn().mockImplementation(() => Promise.resolve()),
dropBackup: jest.fn().mockImplementation(() => Promise.resolve())
cleanup: jest.fn().mockImplementation(() => Promise.resolve())
}

module.exports = () => stub
24 changes: 12 additions & 12 deletions test/__snapshots__/runAll.spec.js.snap
Expand Up @@ -2,8 +2,8 @@

exports[`runAll should not skip tasks if there are files 1`] = `
"
LOG Creating backup... [started]
LOG Creating backup... [completed]
LOG Preparing... [started]
LOG Preparing... [completed]
LOG Running tasks... [started]
LOG Running tasks for *.js [started]
LOG echo \\"sample\\" [started]
Expand All @@ -12,8 +12,8 @@ LOG Running tasks for *.js [completed]
LOG Running tasks... [completed]
LOG Applying modifications... [started]
LOG Applying modifications... [completed]
LOG Cleaning up backup... [started]
LOG Cleaning up backup... [completed]"
LOG Cleaning up... [started]
LOG Cleaning up... [completed]"
`;

exports[`runAll should resolve the promise with no files 1`] = `
Expand All @@ -23,8 +23,8 @@ LOG No staged files found."

exports[`runAll should skip applying unstaged modifications if there are errors during linting 1`] = `
"
LOG Creating backup... [started]
LOG Creating backup... [completed]
LOG Preparing... [started]
LOG Preparing... [completed]
LOG Running tasks... [started]
LOG Running tasks for *.js [started]
LOG echo \\"sample\\" [started]
Expand All @@ -38,8 +38,8 @@ LOG Applying modifications... [skipped]
LOG → Skipped because of errors from tasks.
LOG Reverting to original state because of errors... [started]
LOG Reverting to original state because of errors... [completed]
LOG Cleaning up backup... [started]
LOG Cleaning up backup... [completed]
LOG Cleaning up... [started]
LOG Cleaning up... [completed]
LOG {
name: 'ListrError',
errors: [
Expand All @@ -54,8 +54,8 @@ LOG {

exports[`runAll should skip tasks and restore state if terminated 1`] = `
"
LOG Creating backup... [started]
LOG Creating backup... [completed]
LOG Preparing... [started]
LOG Preparing... [completed]
LOG Running tasks... [started]
LOG Running tasks for *.js [started]
LOG echo \\"sample\\" [started]
Expand All @@ -69,8 +69,8 @@ LOG Applying modifications... [skipped]
LOG → Skipped because of errors from tasks.
LOG Reverting to original state because of errors... [started]
LOG Reverting to original state because of errors... [completed]
LOG Cleaning up backup... [started]
LOG Cleaning up backup... [completed]
LOG Cleaning up... [started]
LOG Cleaning up... [completed]
LOG {
name: 'ListrError',
errors: [
Expand Down
4 changes: 2 additions & 2 deletions test/gitWorkflow.spec.js
Expand Up @@ -65,14 +65,14 @@ describe('gitWorkflow', () => {
}
})

describe('dropBackup', () => {
describe('cleanup', () => {
it('should handle errors', async () => {
const gitWorkflow = new GitWorkflow({
gitDir: cwd,
gitConfigDir: path.resolve(cwd, './.git')
})
const ctx = {}
await expect(gitWorkflow.dropBackup(ctx)).rejects.toThrowErrorMatchingInlineSnapshot(
await expect(gitWorkflow.cleanup(ctx)).rejects.toThrowErrorMatchingInlineSnapshot(
`"lint-staged automatic backup is missing!"`
)
expect(ctx).toEqual({
Expand Down
8 changes: 4 additions & 4 deletions test/runAll.unmocked.2.spec.js
Expand Up @@ -106,17 +106,17 @@ describe('runAll', () => {

expect(console.printHistory()).toMatchInlineSnapshot(`
"
LOG Creating backup... [started]
LOG Creating backup... [failed]
LOG Preparing... [started]
LOG Preparing... [failed]
LOG → Merge state could not be restored due to an error!
LOG Running tasks... [started]
LOG Running tasks... [skipped]
LOG → Skipped because of previous git error.
LOG Applying modifications... [started]
LOG Applying modifications... [skipped]
LOG → Skipped because of previous git error.
LOG Cleaning up backup... [started]
LOG Cleaning up backup... [skipped]
LOG Cleaning up... [started]
LOG Cleaning up... [skipped]
LOG → Skipped because of previous git error.
ERROR
× lint-staged failed due to a git error.
Expand Down

0 comments on commit c386e4c

Please sign in to comment.