diff --git a/lib/file.js b/lib/file.js index 08d6b0c03..de6e79bac 100644 --- a/lib/file.js +++ b/lib/file.js @@ -4,25 +4,10 @@ const debug = require('debug')('lint-staged:file') const fs = require('fs') const { promisify } = require('util') -const fsAccess = promisify(fs.access) const fsReadFile = promisify(fs.readFile) const fsUnlink = promisify(fs.unlink) const fsWriteFile = promisify(fs.writeFile) -/** - * Check if a file exists. Returns the filename if exists. - * @param {String} filename - * @returns {String|Boolean} - */ -const exists = async filename => { - try { - await fsAccess(filename) - return filename - } catch { - return false - } -} - /** * Read contents of a file to buffer * @param {String} filename @@ -44,21 +29,19 @@ const readFile = async (filename, ignoreENOENT = true) => { } /** - * Unlink a file if it exists + * Remove a file * @param {String} filename * @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist */ const unlink = async (filename, ignoreENOENT = true) => { - if (filename) { - debug('Unlinking file `%s`', filename) - try { - await fsUnlink(filename) - } catch (error) { - if (ignoreENOENT && error.code === 'ENOENT') { - debug("File `%s` doesn't exist, ignoring...", filename) - } else { - throw error - } + debug('Removing file `%s`', filename) + try { + await fsUnlink(filename) + } catch (error) { + if (ignoreENOENT && error.code === 'ENOENT') { + debug("File `%s` doesn't exist, ignoring...", filename) + } else { + throw error } } } @@ -74,7 +57,6 @@ const writeFile = async (filename, buffer) => { } module.exports = { - exists, readFile, unlink, writeFile diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index 2b39aca67..d09583104 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -4,18 +4,38 @@ const debug = require('debug')('lint-staged:git') const path = require('path') const execGit = require('./execGit') -const { exists, readFile, unlink, writeFile } = require('./file') +const { readFile, unlink, writeFile } = require('./file') const MERGE_HEAD = 'MERGE_HEAD' const MERGE_MODE = 'MERGE_MODE' const MERGE_MSG = 'MERGE_MSG' +// In git status, renames are presented as `from` -> `to`. +// When diffing, both need to be taken into account, but in some cases on the `to`. +const RENAME = / -> / + +/** + * From list of files, split renames and flatten into two files `from` -> `to`. + * @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 [from, to] = file.split(RENAME) + if (includeRenameFrom) flattened.push(from) + flattened.push(to) + } else { + flattened.push(file) + } + return flattened + }, []) + const STASH = 'lint-staged automatic backup' const PATCH_UNSTAGED = 'lint-staged_unstaged.patch' -const PATCH_UNTRACKED = 'lint-staged_untracked.patch' -const GIT_APPLY_ARGS = ['apply', '-v', '--whitespace=nowarn', '--recount', '--unidiff-zero'] +const GIT_APPLY_ARGS = ['-v', '--whitespace=nowarn', '--recount', '--unidiff-zero'] const GIT_DIFF_ARGS = ['--binary', '--unified=0', '--no-color', '--no-ext-diff', '--patch'] const handleError = (error, ctx) => { @@ -49,19 +69,6 @@ class GitWorkflow { return path.resolve(this.gitConfigDir, `./${filename}`) } - /** - * Check if patch file exists and has content. - * @param {string} filename - */ - async hasPatch(filename) { - const resolved = this.getHiddenFilepath(filename) - const pathIfExists = await exists(resolved) - if (!pathIfExists) return false - const buffer = await readFile(pathIfExists) - const patch = buffer.toString().trim() - return patch.length ? filename : false - } - /** * Get name of backup stash */ @@ -75,6 +82,20 @@ class GitWorkflow { return `stash@{${index}}` } + /** + * Get a list of unstaged deleted files + */ + async getDeletedFiles() { + debug('Getting deleted files...') + const lsFiles = await this.execGit(['ls-files', '--deleted']) + const deletedFiles = lsFiles + .split('\n') + .filter(Boolean) + .map(file => path.resolve(this.gitDir, file)) + debug('Found deleted files:', deletedFiles) + return deletedFiles + } + /** * Save meta information about ongoing git merge */ @@ -108,56 +129,66 @@ class GitWorkflow { } /** - * List and delete untracked files + * 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 cleanUntrackedFiles() { - const lsFiles = await this.execGit(['ls-files', '--others', '--exclude-standard']) - const untrackedFiles = lsFiles + async getPartiallyStagedFiles() { + debug('Getting partially staged files...') + const status = await this.execGit(['status', '--porcelain']) + const partiallyStaged = status .split('\n') - .filter(Boolean) - .map(file => path.resolve(this.gitDir, file)) - await Promise.all(untrackedFiles.map(file => unlink(file))) + .filter(line => { + /** + * See https://git-scm.com/docs/git-status#_short_format + * The first letter of the line represents current index status, + * and second the working tree + */ + const [index, workingTree] = line + return index !== ' ' && workingTree !== ' ' && index !== '?' && workingTree !== '?' + }) + .map(line => line.substr(3)) // Remove first three letters (index, workingTree, and a whitespace) + debug('Found partially staged files:', partiallyStaged) + return partiallyStaged.length ? partiallyStaged : null } /** * Create backup stashes, one of everything and one of only staged changes * Staged files are left in the index for running tasks */ - async stashBackup(ctx) { + async createBackup(ctx) { try { debug('Backing up original state...') - // the `git stash` clears metadata about a possible git merge - // Manually check and backup if necessary - await this.backupMergeStatus() + // 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]) + } // 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 - this.deletedFiles = (await this.execGit(['ls-files', '--deleted'])) - .split('\n') - .filter(Boolean) - .map(file => path.resolve(this.gitDir, file)) + this.deletedFiles = await this.getDeletedFiles() - // Save stash of entire original state, including unstaged and untracked changes. - // `--keep-index leaves only staged files on disk, for tasks.` - await this.execGit(['stash', 'save', '--include-untracked', '--keep-index', STASH]) + // the `git stash` clears metadata about a possible git merge + // Manually check and backup if necessary + await this.backupMergeStatus() - // Restore meta information about ongoing git merge + // Save stash of entire original state, including unstaged and untracked changes + await this.execGit(['stash', 'save', '--include-untracked', STASH]) + await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash()]) + + // Restore meta information about ongoing git merge, cleared by `git stash` await this.restoreMergeStatus() - // There is a bug in git =< 2.13.0 where `--keep-index` resurrects deleted files. - // These files should be listed and deleted before proceeding. - await this.cleanUntrackedFiles() - - // Get a diff of unstaged changes by diffing the saved stash against what's left on disk. - await this.execGit([ - 'diff', - ...GIT_DIFF_ARGS, - `--output=${this.getHiddenFilepath(PATCH_UNSTAGED)}`, - await this.getBackupStash(ctx), - '-R' // Show diff in reverse - ]) + // If stashing resurrected deleted files, clean them out + await Promise.all(this.deletedFiles.map(file => unlink(file))) debug('Done backing up original state!') } catch (error) { @@ -168,83 +199,72 @@ class GitWorkflow { } } + /** + * 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. + */ + ctx.gitHideUnstagedChangesError = true + handleError(error, ctx) + } + } + /** * 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) { - const modifiedFiles = await this.execGit(['ls-files', '--modified']) - if (modifiedFiles) { - debug('Detected files modified by tasks:') - debug(modifiedFiles) - debug('Adding files to index...') - await Promise.all( - // stagedFileChunks includes staged files that lint-staged originally detected. - // Add only these files so any 3rd-party edits to other files won't be included in the commit. - this.stagedFileChunks.map(stagedFiles => this.execGit(['add', ...stagedFiles])) - ) - debug('Done adding files to index!') - } - - const modifiedFilesAfterAdd = await this.execGit(['status', '--porcelain']) - if (!modifiedFilesAfterAdd && !this.allowEmpty) { + debug('Adding task modifications to index...') + await Promise.all( + // stagedFileChunks includes staged files that lint-staged originally detected. + // Add only these files so any 3rd-party edits to other files won't be included in the commit. + this.stagedFileChunks.map(stagedFiles => this.execGit(['add', ...stagedFiles])) + ) + debug('Done adding task modifications to index!') + + const stagedFilesAfterAdd = await this.execGit(['diff', '--name-only', '--cached']) + 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 - ctx.gitApplyEmptyCommit = true + ctx.gitApplyEmptyCommitError = true handleError(new Error('Prevented an empty git commit!'), ctx) } + } - // Restore unstaged changes by applying the diff back. If it at first fails, - // this is probably because of conflicts between task modifications. - // 3-way merge usually fixes this, and in case it doesn't we should just give up and throw. - if (await this.hasPatch(PATCH_UNSTAGED)) { - debug('Restoring unstaged changes...') - const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED) - try { - await this.execGit([...GIT_APPLY_ARGS, unstagedPatch]) - } catch (error) { - debug('Error while restoring changes:') - debug(error) - debug('Retrying with 3-way merge') - - try { - // Retry with `--3way` if normal apply fails - await this.execGit([...GIT_APPLY_ARGS, '--3way', unstagedPatch]) - } catch (error2) { - debug('Error while restoring unstaged changes using 3-way merge:') - debug(error2) - ctx.gitApplyModificationsError = true - handleError( - new Error('Unstaged changes could not be restored due to a merge conflict!'), - ctx - ) - } - } - debug('Done restoring unstaged changes!') - } - - // Restore untracked files by reading from the third commit associated with the backup stash - // See https://stackoverflow.com/a/52357762 + /** + * 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) { + debug('Restoring unstaged changes...') + const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED) try { - const backupStash = await this.getBackupStash(ctx) - const untrackedPatch = this.getHiddenFilepath(PATCH_UNTRACKED) - await this.execGit([ - 'show', - ...GIT_DIFF_ARGS, - '--format=%b', - `--output=${untrackedPatch}`, - `${backupStash}^3` - ]) - if (await this.hasPatch(PATCH_UNTRACKED)) { - await this.execGit([...GIT_APPLY_ARGS, untrackedPatch]) + await this.execGit(['apply', ...GIT_APPLY_ARGS, unstagedPatch]) + } catch (applyError) { + debug('Error while restoring changes:') + debug(applyError) + debug('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) { + debug('Error while restoring unstaged changes using 3-way merge:') + debug(threeWayApplyError) + ctx.gitRestoreUnstagedChangesError = true + handleError( + new Error('Unstaged changes could not be restored due to a merge conflict!'), + ctx + ) } - } catch (error) { - ctx.gitRestoreUntrackedError = true - handleError(error, ctx) } - - // If stashing resurrected deleted files, clean them out - await Promise.all(this.deletedFiles.map(file => unlink(file))) } /** @@ -253,16 +273,21 @@ class GitWorkflow { async restoreOriginalState(ctx) { try { debug('Restoring original state...') - const backupStash = await this.getBackupStash(ctx) await this.execGit(['reset', '--hard', 'HEAD']) - await this.execGit(['stash', 'apply', '--quiet', '--index', backupStash]) - debug('Done restoring original state!') + await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash(ctx)]) + + // Restore meta information about ongoing git merge + await this.restoreMergeStatus() // If stashing resurrected deleted files, clean them out await Promise.all(this.deletedFiles.map(file => unlink(file))) - // Restore meta information about ongoing git merge - await this.restoreMergeStatus() + // Clean out patch + if (this.partiallyStagedFiles) { + await unlink(PATCH_UNSTAGED) + } + + debug('Done restoring original state!') } catch (error) { ctx.gitRestoreOriginalStateError = true handleError(error, ctx) @@ -275,12 +300,7 @@ class GitWorkflow { async dropBackup(ctx) { try { debug('Dropping backup stash...') - await Promise.all([ - exists(this.getHiddenFilepath(PATCH_UNSTAGED)).then(unlink), - exists(this.getHiddenFilepath(PATCH_UNTRACKED)).then(unlink) - ]) - const backupStash = await this.getBackupStash(ctx) - await this.execGit(['stash', 'drop', '--quiet', backupStash]) + await this.execGit(['stash', 'drop', '--quiet', await this.getBackupStash(ctx)]) debug('Done dropping backup stash!') } catch (error) { handleError(error, ctx) diff --git a/lib/runAll.js b/lib/runAll.js index 17f4fb9d4..d167566c4 100644 --- a/lib/runAll.js +++ b/lib/runAll.js @@ -41,14 +41,14 @@ const shouldSkipApplyModifications = ctx => { const shouldSkipRevert = ctx => { // Should be skipped in case of unknown git errors - if (ctx.gitError && !ctx.gitApplyEmptyCommit && !ctx.gitApplyModificationsError) { + if (ctx.gitError && !ctx.gitApplyEmptyCommitError && !ctx.gitRestoreUnstagedChangesError) { return MESSAGES.GIT_ERROR } } const shouldSkipCleanup = ctx => { // Should be skipped in case of unknown git errors - if (ctx.gitError && !ctx.gitApplyEmptyCommit && !ctx.gitApplyModificationsError) { + if (ctx.gitError && !ctx.gitApplyEmptyCommitError && !ctx.gitRestoreUnstagedChangesError) { return MESSAGES.GIT_ERROR } // Should be skipped when reverting to original state fails @@ -188,8 +188,13 @@ const runAll = async ( const runner = new Listr( [ { - title: 'Preparing...', - task: ctx => git.stashBackup(ctx) + title: 'Creating backup...', + task: ctx => git.createBackup(ctx) + }, + { + title: 'Hiding unstaged changes to partially staged files...', + task: ctx => git.hideUnstagedChanges(ctx), + enabled: ctx => ctx.hasPartiallyStagedFiles }, ...listrTasks, { @@ -198,13 +203,20 @@ const runAll = async ( skip: shouldSkipApplyModifications }, { - title: 'Reverting to original state...', + title: 'Restoring unstaged changes to partially staged files...', + task: ctx => git.restoreUnstagedChanges(ctx), + enabled: ctx => ctx.hasPartiallyStagedFiles, + skip: shouldSkipApplyModifications + }, + { + title: 'Reverting to original state because of errors...', task: ctx => git.restoreOriginalState(ctx), - enabled: ctx => ctx.taskError || ctx.gitApplyEmptyCommit || ctx.gitApplyModificationsError, + enabled: ctx => + ctx.taskError || ctx.gitApplyEmptyCommitError || ctx.gitRestoreUnstagedChangesError, skip: shouldSkipRevert }, { - title: 'Cleaning up...', + title: 'Cleaning up backup...', task: ctx => git.dropBackup(ctx), skip: shouldSkipCleanup } @@ -215,7 +227,7 @@ const runAll = async ( try { await runner.run({}) } catch (error) { - if (error.context.gitApplyEmptyCommit) { + if (error.context.gitApplyEmptyCommitError) { logger.warn(` ${symbols.warning} ${chalk.yellow(`lint-staged prevented an empty git commit. Use the --allow-empty option to continue, or check your task configuration`)} diff --git a/test/__snapshots__/runAll.spec.js.snap b/test/__snapshots__/runAll.spec.js.snap index 6eb433110..b47659b28 100644 --- a/test/__snapshots__/runAll.spec.js.snap +++ b/test/__snapshots__/runAll.spec.js.snap @@ -2,8 +2,8 @@ exports[`runAll should not skip tasks if there are files 1`] = ` " -LOG Preparing... [started] -LOG Preparing... [completed] +LOG Creating backup... [started] +LOG Creating backup... [completed] LOG Running tasks... [started] LOG Running tasks for *.js [started] LOG echo \\"sample\\" [started] @@ -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... [started] -LOG Cleaning up... [completed]" +LOG Cleaning up backup... [started] +LOG Cleaning up backup... [completed]" `; exports[`runAll should resolve the promise with no files 1`] = ` @@ -23,8 +23,8 @@ LOG No staged files found." exports[`runAll should skip applying unstaged modifications if there are errors during linting 1`] = ` " -LOG Preparing... [started] -LOG Preparing... [completed] +LOG Creating backup... [started] +LOG Creating backup... [completed] LOG Running tasks... [started] LOG Running tasks for *.js [started] LOG echo \\"sample\\" [started] @@ -36,10 +36,10 @@ LOG Running tasks... [failed] LOG Applying modifications... [started] LOG Applying modifications... [skipped] LOG → Skipped because of errors from tasks. -LOG Reverting to original state... [started] -LOG Reverting to original state... [completed] -LOG Cleaning up... [started] -LOG Cleaning up... [completed] +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 { name: 'ListrError', errors: [ @@ -54,8 +54,8 @@ LOG { exports[`runAll should skip tasks and restore state if terminated 1`] = ` " -LOG Preparing... [started] -LOG Preparing... [completed] +LOG Creating backup... [started] +LOG Creating backup... [completed] LOG Running tasks... [started] LOG Running tasks for *.js [started] LOG echo \\"sample\\" [started] @@ -67,10 +67,10 @@ LOG Running tasks... [failed] LOG Applying modifications... [started] LOG Applying modifications... [skipped] LOG → Skipped because of errors from tasks. -LOG Reverting to original state... [started] -LOG Reverting to original state... [completed] -LOG Cleaning up... [started] -LOG Cleaning up... [completed] +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 { name: 'ListrError', errors: [ diff --git a/test/gitWorkflow.spec.js b/test/gitWorkflow.spec.js index 97d7cb5e9..7ae7b34ed 100644 --- a/test/gitWorkflow.spec.js +++ b/test/gitWorkflow.spec.js @@ -65,16 +65,6 @@ describe('gitWorkflow', () => { } }) - describe('hasPatch', () => { - it('should return false when patch file not found', async () => { - const gitWorkflow = new GitWorkflow({ - gitDir: cwd, - gitConfigDir: path.resolve(cwd, './.git') - }) - expect(await gitWorkflow.hasPatch('foo')).toEqual(false) - }) - }) - describe('dropBackup', () => { it('should handle errors', async () => { const gitWorkflow = new GitWorkflow({ @@ -92,18 +82,22 @@ describe('gitWorkflow', () => { }) }) - describe('cleanUntrackedFiles', () => { - it('should remove untracked files', async () => { - const tempFile = path.resolve(cwd, 'tempFile') - await fs.writeFile(tempFile, 'Hello') - + describe('hideUnstagedChanges', () => { + it('should handle errors', async () => { const gitWorkflow = new GitWorkflow({ gitDir: cwd, gitConfigDir: path.resolve(cwd, './.git') }) - - await gitWorkflow.cleanUntrackedFiles() - await expect(fs.access(tempFile)).rejects.toThrow('ENOENT') + const totallyRandom = `totally_random_file-${Date.now().toString()}` + gitWorkflow.partiallyStagedFiles = [totallyRandom] + const ctx = {} + await expect(gitWorkflow.hideUnstagedChanges(ctx)).rejects.toThrowErrorMatchingInlineSnapshot( + `"error: pathspec '${totallyRandom}' did not match any file(s) known to git"` + ) + expect(ctx).toEqual({ + gitError: true, + gitHideUnstagedChangesError: true + }) }) }) }) diff --git a/test/runAll.unmocked.2.spec.js b/test/runAll.unmocked.2.spec.js index 86aea6588..04c26e1c2 100644 --- a/test/runAll.unmocked.2.spec.js +++ b/test/runAll.unmocked.2.spec.js @@ -106,8 +106,8 @@ describe('runAll', () => { expect(console.printHistory()).toMatchInlineSnapshot(` " - LOG Preparing... [started] - LOG Preparing... [failed] + LOG Creating backup... [started] + LOG Creating backup... [failed] LOG → Merge state could not be restored due to an error! LOG Running tasks... [started] LOG Running tasks... [skipped] @@ -115,8 +115,8 @@ describe('runAll', () => { LOG Applying modifications... [started] LOG Applying modifications... [skipped] LOG → Skipped because of previous git error. - LOG Cleaning up... [started] - LOG Cleaning up... [skipped] + LOG Cleaning up backup... [started] + LOG Cleaning up backup... [skipped] LOG → Skipped because of previous git error. ERROR × lint-staged failed due to a git error. diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js index 88cfd687b..373712441 100644 --- a/test/runAll.unmocked.spec.js +++ b/test/runAll.unmocked.spec.js @@ -234,7 +234,26 @@ describe('runAll', () => { await appendFile('test.js', appended) // Run lint-staged with `prettier --list-different` and commit pretty file - await gitCommit({ config: { '*.js': 'prettier --list-different' } }) + await gitCommit({ config: { '*.js': 'prettier --list-different' }, quiet: false }) + expect(console.printHistory()).toMatchInlineSnapshot(` + " + LOG Creating backup... [started] + LOG Creating backup... [completed] + LOG Hiding unstaged changes to partially staged files... [started] + LOG Hiding unstaged changes to partially staged files... [completed] + LOG Running tasks... [started] + LOG Running tasks for *.js [started] + LOG prettier --list-different [started] + LOG prettier --list-different [completed] + LOG Running tasks for *.js [completed] + LOG Running tasks... [completed] + LOG Applying modifications... [started] + LOG Applying modifications... [completed] + LOG Restoring unstaged changes to partially staged files... [started] + LOG Restoring unstaged changes to partially staged files... [completed] + LOG Cleaning up backup... [started] + LOG Cleaning up backup... [completed]" + `) // Nothing is wrong, so a new commit is created and file is pretty expect(await execGit(['rev-list', '--count', 'HEAD'])).toEqual('2') @@ -291,11 +310,34 @@ describe('runAll', () => { const status = await execGit(['status']) // Run lint-staged with `prettier --list-different` to break the linter - try { - await gitCommit({ config: { '*.js': 'prettier --list-different' } }) - } catch (error) { - expect(error.message).toMatchInlineSnapshot(`"Something went wrong"`) - } + await expect( + gitCommit({ config: { '*.js': 'prettier --list-different' }, quiet: false }) + ).rejects.toThrowError() + expect(console.printHistory()).toMatchInlineSnapshot(` + " + LOG Creating backup... [started] + LOG Creating backup... [completed] + LOG Hiding unstaged changes to partially staged files... [started] + LOG Hiding unstaged changes to partially staged files... [completed] + LOG Running tasks... [started] + LOG Running tasks for *.js [started] + LOG prettier --list-different [started] + LOG prettier --list-different [failed] + LOG → + LOG Running tasks for *.js [failed] + LOG → + LOG Running tasks... [failed] + LOG Applying modifications... [started] + LOG Applying modifications... [skipped] + LOG → Skipped because of errors from tasks. + LOG Restoring unstaged changes to partially staged files... [started] + LOG Restoring unstaged changes to partially staged files... [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]" + `) // Something was wrong so the repo is returned to original state expect(await execGit(['rev-list', '--count', 'HEAD'])).toEqual('1') @@ -519,11 +561,7 @@ describe('runAll', () => { expect(await execGit(['log', '-1', '--pretty=%B'])).toMatch('commit a') // Merge second branch, causing merge conflict - try { - await execGit(['merge', 'branch-b']) - } catch (error) { - expect(error.message).toMatch('Merge conflict in test.js') - } + await expect(execGit(['merge', 'branch-b'])).rejects.toThrowError('Merge conflict in test.js') expect(await readFile('test.js')).toMatchInlineSnapshot(` "<<<<<<< HEAD @@ -706,8 +744,8 @@ describe('runAll', () => { expect(console.printHistory()).toMatchInlineSnapshot(` " - LOG Preparing... [started] - LOG Preparing... [completed] + LOG Creating backup... [started] + LOG Creating backup... [completed] LOG Running tasks... [started] LOG Running tasks for *.js [started] LOG [Function] git ... [started] @@ -715,11 +753,10 @@ describe('runAll', () => { LOG Running tasks for *.js [completed] LOG Running tasks... [completed] LOG Applying modifications... [started] - LOG Applying modifications... [failed] - LOG → lint-staged automatic backup is missing! - LOG Cleaning up... [started] - LOG Cleaning up... [skipped] - LOG → Skipped because of previous git error." + LOG Applying modifications... [completed] + LOG Cleaning up backup... [started] + LOG Cleaning up backup... [failed] + LOG → lint-staged automatic backup is missing!" `) })