diff --git a/lib/gitWorkflow.js b/lib/gitWorkflow.js index f83d2a293..582b38efb 100644 --- a/lib/gitWorkflow.js +++ b/lib/gitWorkflow.js @@ -166,7 +166,7 @@ class GitWorkflow { /** * Create a diff of partially staged files and backup stash if enabled. */ - async prepare(ctx, stash) { + async prepare(ctx, shouldBackup) { try { debug('Backing up original state...') @@ -184,7 +184,7 @@ class GitWorkflow { /** * If backup stash should be skipped, no need to continue */ - if (!stash) return + if (!shouldBackup) 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 @@ -207,9 +207,6 @@ class GitWorkflow { debug('Done backing up original state!') } catch (error) { - if (error.message && error.message.includes('You do not have the initial commit yet')) { - ctx.emptyGitRepo = true - } handleError(error, ctx) } } diff --git a/lib/runAll.js b/lib/runAll.js index 59a44f7a8..993d63172 100644 --- a/lib/runAll.js +++ b/lib/runAll.js @@ -7,6 +7,7 @@ const Listr = require('listr') const symbols = require('log-symbols') const chunkFiles = require('./chunkFiles') +const execGit = require('./execGit') const generateTasks = require('./generateTasks') const getStagedFiles = require('./getStagedFiles') const GitWorkflow = require('./gitWorkflow') @@ -91,15 +92,22 @@ const runAll = async ( ) => { 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!') + // Test whether we have any commits or not. + // Stashing must be disabled with no initial commit. + const hasInitialCommit = await execGit(['log', '-1'], { cwd: gitDir }) + .then(() => true) + .catch(() => false) + + // Lint-staged should create a backup stash only when there's an initial commit + const shouldBackup = hasInitialCommit && stash + if (!shouldBackup) { + const reason = hasInitialCommit ? '`--no-stash` was used' : 'there’s no initial commit yet' + logger.warn(`${symbols.warning} ${chalk.yellow(`Skipping backup because ${reason}.\n`)}`) + } + const files = await getStagedFiles({ cwd: gitDir }) if (!files) throw new Error('Unable to get staged files!') debugLog('Loaded list of staged files in git:\n%O', files) @@ -202,7 +210,7 @@ const runAll = async ( [ { title: 'Preparing...', - task: ctx => git.prepare(ctx, stash) + task: ctx => git.prepare(ctx, shouldBackup) }, { title: 'Hiding unstaged changes to partially staged files...', @@ -214,7 +222,7 @@ const runAll = async ( title: 'Applying modifications...', task: ctx => git.applyModifications(ctx), // Always apply back unstaged modifications when skipping backup - skip: ctx => stash && shouldSkipApplyModifications(ctx) + skip: ctx => shouldBackup && shouldSkipApplyModifications(ctx) }, { title: 'Restoring unstaged changes to partially staged files...', @@ -226,14 +234,14 @@ const runAll = async ( title: 'Reverting to original state because of errors...', task: ctx => git.restoreOriginalState(ctx), enabled: ctx => - stash && + shouldBackup && (ctx.taskError || ctx.gitApplyEmptyCommitError || ctx.gitRestoreUnstagedChangesError), skip: shouldSkipRevert }, { title: 'Cleaning up...', task: ctx => git.cleanup(ctx), - enabled: () => stash, + enabled: () => shouldBackup, skip: shouldSkipCleanup } ], @@ -251,12 +259,7 @@ const runAll = async ( } else if (error.context.gitError && !error.context.gitGetBackupStashError) { logger.error(`\n ${symbols.error} ${chalk.red(`lint-staged failed due to a git error.`)}`) - if (error.context.emptyGitRepo) { - logger.error( - `\n The initial commit is needed for lint-staged to work. - Please use the --no-verify flag to skip running lint-staged.` - ) - } else if (stash) { + if (shouldBackup) { // No sense to show this if the backup stash itself is missing. logger.error(` Any lost modifications can be restored from a git stash: diff --git a/test/__snapshots__/runAll.spec.js.snap b/test/__snapshots__/runAll.spec.js.snap deleted file mode 100644 index c0f1bb48d..000000000 --- a/test/__snapshots__/runAll.spec.js.snap +++ /dev/null @@ -1,89 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`runAll should not skip tasks if there are files 1`] = ` -" -LOG Preparing... [started] -LOG Preparing... [completed] -LOG Running tasks... [started] -LOG Running tasks for *.js [started] -LOG echo \\"sample\\" [started] -LOG echo \\"sample\\" [completed] -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]" -`; - -exports[`runAll should resolve the promise with no files 1`] = ` -" -LOG i 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 Running tasks... [started] -LOG Running tasks for *.js [started] -LOG echo \\"sample\\" [started] -LOG echo \\"sample\\" [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 Reverting to original state because of errors... [started] -LOG Reverting to original state because of errors... [completed] -LOG Cleaning up... [started] -LOG Cleaning up... [completed] -LOG { - name: 'ListrError', - errors: [ - { - privateMsg: '\\\\n\\\\n\\\\n× echo found some errors. Please fix them and try committing again.\\\\n\\\\nLinter finished with error', - context: {taskError: true} - } - ], - context: {taskError: true} -}" -`; - -exports[`runAll should skip tasks and restore state if terminated 1`] = ` -" -LOG Preparing... [started] -LOG Preparing... [completed] -LOG Running tasks... [started] -LOG Running tasks for *.js [started] -LOG echo \\"sample\\" [started] -LOG echo \\"sample\\" [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 Reverting to original state because of errors... [started] -LOG Reverting to original state because of errors... [completed] -LOG Cleaning up... [started] -LOG Cleaning up... [completed] -LOG { - name: 'ListrError', - errors: [ - { - privateMsg: '\\\\n\\\\n\\\\n‼ echo was terminated with SIGINT', - context: {taskError: true} - } - ], - context: {taskError: true} -}" -`; - -exports[`runAll should use an injected logger 1`] = ` -" -LOG i No staged files found." -`; diff --git a/test/runAll.spec.js b/test/runAll.spec.js index ffdf0387a..fe6009dcb 100644 --- a/test/runAll.spec.js +++ b/test/runAll.spec.js @@ -17,9 +17,9 @@ resolveGitRepo.mockImplementation(async () => { }) getStagedFiles.mockImplementation(async () => []) -const globalConsoleTemp = console - describe('runAll', () => { + const globalConsoleTemp = console + beforeAll(() => { console = makeConsoleMock() }) @@ -33,30 +33,52 @@ describe('runAll', () => { }) it('should resolve the promise with no tasks', async () => { - await expect(runAll({ config: {} })).resolves + expect.assertions(1) + await expect(runAll({ config: {} })).resolves.toEqual(undefined) }) it('should resolve the promise with no files', async () => { + expect.assertions(1) await runAll({ config: { '*.js': ['echo "sample"'] } }) - expect(console.printHistory()).toMatchSnapshot() + expect(console.printHistory()).toMatchInlineSnapshot(` + " + LOG i No staged files found." + `) }) it('should use an injected logger', async () => { expect.assertions(1) const logger = makeConsoleMock() await runAll({ config: { '*.js': ['echo "sample"'] }, debug: true }, logger) - expect(logger.printHistory()).toMatchSnapshot() + expect(logger.printHistory()).toMatchInlineSnapshot(` + " + LOG i No staged files found." + `) }) it('should not skip tasks if there are files', async () => { expect.assertions(1) getStagedFiles.mockImplementationOnce(async () => ['sample.js']) await runAll({ config: { '*.js': ['echo "sample"'] } }) - expect(console.printHistory()).toMatchSnapshot() + expect(console.printHistory()).toMatchInlineSnapshot(` + " + LOG Preparing... [started] + LOG Preparing... [completed] + LOG Running tasks... [started] + LOG Running tasks for *.js [started] + LOG echo \\"sample\\" [started] + LOG echo \\"sample\\" [completed] + 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]" + `) }) it('should skip applying unstaged modifications if there are errors during linting', async () => { - expect.assertions(1) + expect.assertions(2) getStagedFiles.mockImplementationOnce(async () => ['sample.js']) execa.mockImplementation(() => Promise.resolve({ @@ -68,12 +90,30 @@ describe('runAll', () => { }) ) - try { - await runAll({ config: { '*.js': ['echo "sample"'] } }) - } catch (err) { - console.log(err) - } - expect(console.printHistory()).toMatchSnapshot() + await expect( + runAll({ config: { '*.js': ['echo "sample"'] } }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Something went wrong"`) + + expect(console.printHistory()).toMatchInlineSnapshot(` + " + LOG Preparing... [started] + LOG Preparing... [completed] + LOG Running tasks... [started] + LOG Running tasks for *.js [started] + LOG echo \\"sample\\" [started] + LOG echo \\"sample\\" [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 Reverting to original state because of errors... [started] + LOG Reverting to original state because of errors... [completed] + LOG Cleaning up... [started] + LOG Cleaning up... [completed]" + `) }) it('should skip tasks and restore state if terminated', async () => { @@ -96,7 +136,37 @@ describe('runAll', () => { } catch (err) { console.log(err) } - expect(console.printHistory()).toMatchSnapshot() + + expect(console.printHistory()).toMatchInlineSnapshot(` + " + LOG Preparing... [started] + LOG Preparing... [completed] + LOG Running tasks... [started] + LOG Running tasks for *.js [started] + LOG echo \\"sample\\" [started] + LOG echo \\"sample\\" [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 Reverting to original state because of errors... [started] + LOG Reverting to original state because of errors... [completed] + LOG Cleaning up... [started] + LOG Cleaning up... [completed] + LOG { + name: 'ListrError', + errors: [ + { + privateMsg: '\\\\n\\\\n\\\\n‼ echo was terminated with SIGINT', + context: {taskError: true} + } + ], + context: {taskError: true} + }" + `) }) }) diff --git a/test/runAll.unmocked.spec.js b/test/runAll.unmocked.spec.js index 3a7a82217..7f6ffec7f 100644 --- a/test/runAll.unmocked.spec.js +++ b/test/runAll.unmocked.spec.js @@ -5,11 +5,11 @@ import os from 'os' import path from 'path' import nanoid from 'nanoid' +jest.unmock('execa') + import execGitBase from '../lib/execGit' import runAll from '../lib/runAll' -jest.unmock('execa') - jest.setTimeout(20000) const testJsFilePretty = `module.exports = { @@ -70,8 +70,8 @@ const execGit = async (args, options = {}) => execGitBase(args, { cwd, ...option // Execute runAll before git commit to emulate lint-staged const gitCommit = async (options, args = ['-m test']) => { - await runAll({ quiet: true, ...options, cwd }) - await execGit(['commit', ...args]) + await runAll({ quiet: true, cwd, ...options }) + await execGit(['commit', ...args], { cwd, ...options }) } describe('runAll', () => { @@ -945,6 +945,7 @@ describe('runAll', () => { expect(console.printHistory()).toMatchInlineSnapshot(` " WARN ‼ Skipping backup because \`--no-stash\` was used. + LOG Preparing... [started] LOG Preparing... [completed] LOG Running tasks... [started] @@ -986,6 +987,7 @@ describe('runAll', () => { expect(console.printHistory()).toMatchInlineSnapshot(` " WARN ‼ Skipping backup because \`--no-stash\` was used. + LOG Preparing... [started] LOG Preparing... [completed] LOG Hiding unstaged changes to partially staged files... [started] @@ -1041,6 +1043,7 @@ describe('runAll', () => { expect(console.printHistory()).toMatchInlineSnapshot(` " WARN ‼ Skipping backup because \`--no-stash\` was used. + LOG Preparing... [started] LOG Preparing... [completed] LOG Running tasks... [started] @@ -1072,26 +1075,49 @@ describe('runAll', () => { }) describe('runAll', () => { - it('Should throw when run on an empty git repo without an initial commit', async () => { + it('Should skip backup when run on an empty git repo without an initial commit', async () => { + const globalConsoleTemp = console + console = makeConsoleMock() const tmpDir = await createTempDir() const cwd = normalize(tmpDir) - const logger = makeConsoleMock() await execGit('init', { cwd }) await execGit(['config', 'user.name', '"test"'], { cwd }) await execGit(['config', 'user.email', '"test@test.com"'], { cwd }) await appendFile('test.js', testJsFilePretty, cwd) await execGit(['add', 'test.js'], { cwd }) - await expect( - runAll({ config: { '*.js': 'prettier --list-different' }, cwd, quiet: true }, logger) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Something went wrong"`) - expect(logger.printHistory()).toMatchInlineSnapshot(` + + await expect(execGit(['log', '-1'], { cwd })).rejects.toThrowErrorMatchingInlineSnapshot( + `"fatal: your current branch 'master' does not have any commits yet"` + ) + + await gitCommit({ + config: { '*.js': 'prettier --list-different' }, + cwd, + debut: true, + quiet: false + }) + + expect(console.printHistory()).toMatchInlineSnapshot(` " - ERROR - × lint-staged failed due to a git error. - ERROR - The initial commit is needed for lint-staged to work. - Please use the --no-verify flag to skip running lint-staged." + WARN ‼ Skipping backup because there’s no initial commit yet. + + LOG Preparing... [started] + LOG Preparing... [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]" `) + + // Nothing is wrong, so the initial commit is created + expect(await execGit(['rev-list', '--count', 'HEAD'], { cwd })).toEqual('1') + expect(await execGit(['log', '-1', '--pretty=%B'], { cwd })).toMatch('test') + expect(await readFile('test.js', cwd)).toEqual(testJsFilePretty) + console = globalConsoleTemp }) })