diff --git a/__mocks__/execa.js b/__mocks__/execa.js index 347d08e..7fcf523 100644 --- a/__mocks__/execa.js +++ b/__mocks__/execa.js @@ -1,15 +1,10 @@ -const mockStream = () => ({ - once: jest.fn(), - on: jest.fn(), - removeListener: jest.fn(), - pipe: jest.fn(), -}); - -const mockExeca = jest.fn().mockReturnValue({ - stdout: mockStream(), - stderr: mockStream(), - kill: () => {}, -}); +const mockExeca = jest.fn().mockReturnValue( + Promise.resolve({ + stdout: '', + stderr: '', + kill: () => {}, + }), +); const mockExecaSync = jest.fn().mockReturnValue({ stdout: '', diff --git a/__mocks__/prettier.js b/__mocks__/prettier.js index 05b8698..d3290b9 100644 --- a/__mocks__/prettier.js +++ b/__mocks__/prettier.js @@ -1,20 +1,33 @@ const path = require('path'); +const resolveConfigMock = jest.fn().mockImplementation((file) => + Promise.resolve({ + file, + }), +); +resolveConfigMock.sync = jest.fn().mockImplementation((file) => ({ file })); + +const getFileInfoMock = jest.fn().mockImplementation((file) => { + const ext = path.extname(file); + if (ext === '.js' || ext === '.md') { + return Promise.resolve({ ignored: false, inferredParser: 'babel' }); + } else { + return Promise.resolve({ ignored: false, inferredParser: null }); + } +}); +getFileInfoMock.sync = jest.fn().mockImplementation((file) => { + const ext = path.extname(file); + if (ext === '.js' || ext === '.md') { + return { ignored: false, inferredParser: 'babel' }; + } else { + return { ignored: false, inferredParser: null }; + } +}); + const prettierMock = { format: jest.fn().mockImplementation((input) => 'formatted:' + input), - resolveConfig: { - sync: jest.fn().mockImplementation((file) => ({ file })), - }, - getFileInfo: { - sync: jest.fn().mockImplementation((file) => { - const ext = path.extname(file); - if (ext === '.js' || ext === '.md') { - return { ignored: false, inferredParser: 'babel' }; - } else { - return { ignored: false, inferredParser: null }; - } - }), - }, + resolveConfig: resolveConfigMock, + getFileInfo: getFileInfoMock, }; module.exports = prettierMock; diff --git a/bin/pretty-quick.js b/bin/pretty-quick.js index 541745c..cea372c 100755 --- a/bin/pretty-quick.js +++ b/bin/pretty-quick.js @@ -14,63 +14,74 @@ const args = mri(process.argv.slice(2), { }, }); -const prettyQuickResult = prettyQuick( - process.cwd(), - Object.assign({}, args, { - onFoundSinceRevision: (scm, revision) => { - console.log( - `🔍 Finding changed files since ${chalk.bold( - scm, - )} revision ${chalk.bold(revision)}.`, - ); - }, +(async () => { + const prettyQuickResult = await prettyQuick( + process.cwd(), + Object.assign({}, args, { + onFoundSinceRevision: (scm, revision) => { + console.log( + `🔍 Finding changed files since ${chalk.bold( + scm, + )} revision ${chalk.bold(revision)}.`, + ); + }, - onFoundChangedFiles: (changedFiles) => { - console.log( - `🎯 Found ${chalk.bold(changedFiles.length)} changed ${ - changedFiles.length === 1 ? 'file' : 'files' - }.`, - ); - }, + onFoundChangedFiles: (changedFiles) => { + console.log( + `🎯 Found ${chalk.bold(changedFiles.length)} changed ${ + changedFiles.length === 1 ? 'file' : 'files' + }.`, + ); + }, - onPartiallyStagedFile: (file) => { - console.log(`✗ Found ${chalk.bold('partially')} staged file ${file}.`); - }, + onPartiallyStagedFile: (file) => { + console.log(`✗ Found ${chalk.bold('partially')} staged file ${file}.`); + }, - onWriteFile: (file) => { - console.log(`✍️ Fixing up ${chalk.bold(file)}.`); - }, + onWriteFile: (file) => { + console.log(`✍️ Fixing up ${chalk.bold(file)}.`); + }, - onCheckFile: (file, isFormatted) => { - if (!isFormatted) { - console.log(`⛔️ Check failed: ${chalk.bold(file)}`); - } - }, + onCheckFile: (file, isFormatted) => { + if (!isFormatted) { + console.log(`⛔️ Check failed: ${chalk.bold(file)}`); + } + }, - onExamineFile: (file) => { - console.log(`🔍 Examining ${chalk.bold(file)}.`); - }, - }), -); + onExamineFile: (file) => { + console.log(`🔍 Examining ${chalk.bold(file)}.`); + }, -if (prettyQuickResult.success) { - console.log('✅ Everything is awesome!'); -} else { - if (prettyQuickResult.errors.indexOf('PARTIALLY_STAGED_FILE') !== -1) { - console.log( - '✗ Partially staged files were fixed up.' + - ` ${chalk.bold('Please update stage before committing')}.`, - ); - } - if (prettyQuickResult.errors.indexOf('BAIL_ON_WRITE') !== -1) { - console.log( - '✗ File had to be prettified and prettyQuick was set to bail mode.', - ); - } - if (prettyQuickResult.errors.indexOf('CHECK_FAILED') !== -1) { - console.log( - '✗ Code style issues found in the above file(s). Forgot to run Prettier?', - ); + onStageFiles: () => { + console.log(`🏗️ Staging changed files.`); + }, + }), + ); + + if (prettyQuickResult.success) { + console.log('✅ Everything is awesome!'); + } else { + if (prettyQuickResult.errors.indexOf('PARTIALLY_STAGED_FILE') !== -1) { + console.log( + '✗ Partially staged files were fixed up.' + + ` ${chalk.bold('Please update stage before committing')}.`, + ); + } + if (prettyQuickResult.errors.indexOf('BAIL_ON_WRITE') !== -1) { + console.log( + '✗ File had to be prettified and prettyQuick was set to bail mode.', + ); + } + if (prettyQuickResult.errors.indexOf('CHECK_FAILED') !== -1) { + console.log( + '✗ Code style issues found in the above file(s). Forgot to run Prettier?', + ); + } + if (prettyQuickResult.errors.indexOf('STAGE_FAILED') !== -1) { + console.log( + '✗ Failed to stage some or all of the above file(s). Please stage changes made by Prettier before committing.', + ); + } + process.exit(1); // ensure git hooks abort } - process.exit(1); // ensure git hooks abort -} +})(); diff --git a/src/__tests__/pretty-quick.test.js b/src/__tests__/pretty-quick.test.js index c917601..d89b0a5 100644 --- a/src/__tests__/pretty-quick.test.js +++ b/src/__tests__/pretty-quick.test.js @@ -6,12 +6,12 @@ jest.mock('execa'); afterEach(() => mock.restore()); -test('throws an error when no vcs is found', () => { +test('throws an error when no vcs is found', async () => { mock({ 'root/README.md': '', }); - expect(() => prettyQuick('root')).toThrow( + await expect(prettyQuick('root')).rejects.toThrow( 'Unable to detect a source control manager.', ); }); diff --git a/src/__tests__/scm-git.test.js b/src/__tests__/scm-git.test.js index 0b864aa..a74f63b 100644 --- a/src/__tests__/scm-git.test.js +++ b/src/__tests__/scm-git.test.js @@ -23,7 +23,7 @@ const mockGitFs = (additionalUnstaged = '', additionalFiles = {}) => { additionalFiles, ), ); - execa.sync.mockImplementation((command, args) => { + execa.mockImplementation(async (command, args) => { if (command !== 'git') { throw new Error(`unexpected command: ${command}`); } @@ -43,135 +43,139 @@ const mockGitFs = (additionalUnstaged = '', additionalFiles = {}) => { }; describe('with git', () => { - test('calls `git merge-base`', () => { + test('calls `git merge-base`', async () => { mock({ '/.git': {}, }); - prettyQuick('root'); + await prettyQuick('/'); - expect(execa.sync).toHaveBeenCalledWith( + expect(execa).toHaveBeenCalledWith( 'git', ['merge-base', 'HEAD', 'master'], { cwd: '/' }, ); }); - test('calls `git merge-base` with root git directory', () => { + test('calls `git merge-base` with root git directory', async () => { mock({ '/.git': {}, '/other-dir': {}, }); - prettyQuick('/other-dir'); + await prettyQuick('/other-dir'); - expect(execa.sync).toHaveBeenCalledWith( + expect(execa).toHaveBeenCalledWith( 'git', ['merge-base', 'HEAD', 'master'], { cwd: '/' }, ); }); - test('with --staged does NOT call `git merge-base`', () => { + test('with --staged does NOT call `git merge-base`', async () => { mock({ '/.git': {}, }); - prettyQuick('root'); + await prettyQuick('/'); - expect(execa.sync).not.toHaveBeenCalledWith('git', [ + expect(execa).not.toHaveBeenCalledWith('git', [ 'merge-base', 'HEAD', 'master', ]); }); - test('with --staged calls diff without revision', () => { + test('with --staged calls diff without revision', async () => { mock({ '/.git': {}, }); - prettyQuick('root', { since: 'banana', staged: true }); + await prettyQuick('/', { since: 'banana', staged: true }); - expect(execa.sync).toHaveBeenCalledWith( + expect(execa).toHaveBeenCalledWith( 'git', ['diff', '--name-only', '--diff-filter=ACMRTUB'], { cwd: '/' }, ); }); - test('calls `git diff --name-only` with revision', () => { + test('calls `git diff --name-only` with revision', async () => { mock({ '/.git': {}, }); - prettyQuick('root', { since: 'banana' }); + await prettyQuick('/', { since: 'banana' }); - expect(execa.sync).toHaveBeenCalledWith( + expect(execa).toHaveBeenCalledWith( 'git', ['diff', '--name-only', '--diff-filter=ACMRTUB', 'banana'], { cwd: '/' }, ); }); - test('calls `git ls-files`', () => { + test('calls `git ls-files`', async () => { mock({ '/.git': {}, }); - prettyQuick('root', { since: 'banana' }); + await prettyQuick('/', { since: 'banana' }); - expect(execa.sync).toHaveBeenCalledWith( + expect(execa).toHaveBeenCalledWith( 'git', ['ls-files', '--others', '--exclude-standard'], { cwd: '/' }, ); }); - test('calls onFoundSinceRevision with return value from `git merge-base`', () => { + test('calls onFoundSinceRevision with return value from `git merge-base`', async () => { const onFoundSinceRevision = jest.fn(); mock({ '/.git': {}, }); - execa.sync.mockReturnValue({ stdout: 'banana' }); + execa.mockReturnValue(Promise.resolve({ stdout: 'banana' })); - prettyQuick('root', { onFoundSinceRevision }); + await prettyQuick('/', { onFoundSinceRevision }); expect(onFoundSinceRevision).toHaveBeenCalledWith('git', 'banana'); }); - test('calls onFoundChangedFiles with changed files', () => { + test('calls onFoundChangedFiles with changed files', async () => { const onFoundChangedFiles = jest.fn(); mockGitFs(); - prettyQuick('root', { since: 'banana', onFoundChangedFiles }); + await prettyQuick('/', { since: 'banana', onFoundChangedFiles }); expect(onFoundChangedFiles).toHaveBeenCalledWith(['./foo.js', './bar.md']); }); - test('calls onWriteFile with changed files', () => { + test('calls onWriteFile with changed files', async () => { const onWriteFile = jest.fn(); mockGitFs(); - prettyQuick('root', { since: 'banana', onWriteFile }); + await prettyQuick('/', { since: 'banana', onWriteFile }); expect(onWriteFile).toHaveBeenCalledWith('./foo.js'); expect(onWriteFile).toHaveBeenCalledWith('./bar.md'); expect(onWriteFile.mock.calls.length).toBe(2); }); - test('calls onWriteFile with changed files for the given pattern', () => { + test('calls onWriteFile with changed files for the given pattern', async () => { const onWriteFile = jest.fn(); mockGitFs(); - prettyQuick('root', { pattern: '*.md', since: 'banana', onWriteFile }); + await prettyQuick('/', { + pattern: '*.md', + since: 'banana', + onWriteFile, + }); expect(onWriteFile.mock.calls).toEqual([['./bar.md']]); }); - test('calls onWriteFile with changed files for the given globstar pattern', () => { + test('calls onWriteFile with changed files for the given globstar pattern', async () => { const onWriteFile = jest.fn(); mockGitFs(); - prettyQuick('root', { + await prettyQuick('/', { pattern: '**/*.md', since: 'banana', onWriteFile, @@ -179,10 +183,10 @@ describe('with git', () => { expect(onWriteFile.mock.calls).toEqual([['./bar.md']]); }); - test('calls onWriteFile with changed files for the given extglob pattern', () => { + test('calls onWriteFile with changed files for the given extglob pattern', async () => { const onWriteFile = jest.fn(); mockGitFs(); - prettyQuick('root', { + await prettyQuick('/', { pattern: '*.*(md|foo|bar)', since: 'banana', onWriteFile, @@ -190,10 +194,10 @@ describe('with git', () => { expect(onWriteFile.mock.calls).toEqual([['./bar.md']]); }); - test('calls onWriteFile with changed files for an array of globstar patterns', () => { + test('calls onWriteFile with changed files for an array of globstar patterns', async () => { const onWriteFile = jest.fn(); mockGitFs(); - prettyQuick('root', { + await prettyQuick('/', { pattern: ['**/*.foo', '**/*.md', '**/*.bar'], since: 'banana', onWriteFile, @@ -201,144 +205,155 @@ describe('with git', () => { expect(onWriteFile.mock.calls).toEqual([['./bar.md']]); }); - test('writes formatted files to disk', () => { + test('writes formatted files to disk', async () => { const onWriteFile = jest.fn(); mockGitFs(); - prettyQuick('root', { since: 'banana', onWriteFile }); + await prettyQuick('/', { since: 'banana', onWriteFile }); expect(fs.readFileSync('/foo.js', 'utf8')).toEqual('formatted:foo()'); expect(fs.readFileSync('/bar.md', 'utf8')).toEqual('formatted:# foo'); }); - test('succeeds if a file was changed and bail is not set', () => { + test('succeeds if a file was changed and bail is not set', async () => { mockGitFs(); - const result = prettyQuick('root', { since: 'banana' }); + const result = await prettyQuick('/', { since: 'banana' }); expect(result).toEqual({ errors: [], success: true }); }); - test('fails if a file was changed and bail is set to true', () => { + test('fails if a file was changed and bail is set to true', async () => { mockGitFs(); - const result = prettyQuick('root', { since: 'banana', bail: true }); + const result = await prettyQuick('/', { since: 'banana', bail: true }); expect(result).toEqual({ errors: ['BAIL_ON_WRITE'], success: false }); }); - test('with --staged stages fully-staged files', () => { + test('with --staged stages fully-staged files', async () => { mockGitFs(); - prettyQuick('root', { since: 'banana', staged: true }); + await prettyQuick('/', { since: 'banana', staged: true }); - expect(execa.sync).toHaveBeenCalledWith('git', ['add', './raz.js'], { + expect(execa).toHaveBeenCalledWith('git', ['add', './raz.js'], { cwd: '/', }); - expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { + expect(execa).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { cwd: '/', }); - expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { + expect(execa).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { cwd: '/', }); }); - test('with --staged AND --no-restage does not re-stage any files', () => { + test('with --staged AND --no-restage does not re-stage any files', async () => { mockGitFs(); - prettyQuick('root', { since: 'banana', staged: true, restage: false }); + await prettyQuick('/', { + since: 'banana', + staged: true, + restage: false, + }); - expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './raz.js'], { + expect(execa).not.toHaveBeenCalledWith('git', ['add', './raz.js'], { cwd: '/', }); - expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { + expect(execa).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { cwd: '/', }); - expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { + expect(execa).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { cwd: '/', }); }); - test('with --staged does not stage previously partially staged files AND aborts commit', () => { + test('with --staged does not stage previously partially staged files AND aborts commit', async () => { const additionalUnstaged = './raz.js\n'; // raz.js is partly staged and partly not staged mockGitFs(additionalUnstaged); - prettyQuick('root', { since: 'banana', staged: true }); + await prettyQuick('/', { since: 'banana', staged: true }); - expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './raz.js'], { + expect(execa).not.toHaveBeenCalledWith('git', ['add', './raz.js'], { cwd: '/', }); }); - test('with --staged returns false', () => { + test('with --staged returns false', async () => { const additionalUnstaged = './raz.js\n'; // raz.js is partly staged and partly not staged mockGitFs(additionalUnstaged); - const result = prettyQuick('root', { since: 'banana', staged: true }); + const result = await prettyQuick('/', { since: 'banana', staged: true }); expect(result).toEqual({ errors: ['PARTIALLY_STAGED_FILE'], success: false, }); }); - test('without --staged does NOT stage changed files', () => { + test('without --staged does NOT stage changed files', async () => { mockGitFs(); - prettyQuick('root', { since: 'banana' }); + await prettyQuick('/', { since: 'banana' }); - expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { + expect(execa).not.toHaveBeenCalledWith('git', ['add', './foo.js'], { cwd: '/', }); - expect(execa.sync).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { + expect(execa).not.toHaveBeenCalledWith('git', ['add', './bar.md'], { cwd: '/', }); }); - test('with --verbose calls onExamineFile', () => { + test('with --verbose calls onExamineFile', async () => { const onExamineFile = jest.fn(); mockGitFs(); - prettyQuick('root', { since: 'banana', verbose: true, onExamineFile }); + await prettyQuick('/', { + since: 'banana', + verbose: true, + onExamineFile, + }); expect(onExamineFile).toHaveBeenCalledWith('./foo.js'); expect(onExamineFile).toHaveBeenCalledWith('./bar.md'); }); - test('without --verbose does NOT call onExamineFile', () => { + test('without --verbose does NOT call onExamineFile', async () => { const onExamineFile = jest.fn(); mockGitFs(); - prettyQuick('root', { since: 'banana', onExamineFile }); + await prettyQuick('/', { since: 'banana', onExamineFile }); expect(onExamineFile).not.toHaveBeenCalledWith('./foo.js'); expect(onExamineFile).not.toHaveBeenCalledWith('./bar.md'); }); - test('ignore files matching patterns from the repositories root .prettierignore', () => { + test('ignore files matching patterns from the repositories root .prettierignore', async () => { const onWriteFile = jest.fn(); mockGitFs('', { '/.prettierignore': '*.md', }); - prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }); + await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }); expect(onWriteFile.mock.calls).toEqual([['./foo.js']]); }); - test('ignore files matching patterns from the working directories .prettierignore', () => { + test('ignore files matching patterns from the working directories .prettierignore', async () => { const onWriteFile = jest.fn(); mockGitFs('', { '/sub-directory/.prettierignore': '*.md', }); - prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }); + await prettyQuick('/sub-directory/', { + since: 'banana', + onWriteFile, + }); expect(onWriteFile.mock.calls).toEqual([['./foo.js']]); }); - test('with --ignore-path to ignore files matching patterns from the repositories root .ignorePath', () => { + test('with --ignore-path to ignore files matching patterns from the repositories root .ignorePath', async () => { const onWriteFile = jest.fn(); mockGitFs('', { '/.ignorePath': '*.md', }); - prettyQuick('/sub-directory/', { + await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile, ignorePath: '/.ignorePath', @@ -346,12 +361,12 @@ describe('with git', () => { expect(onWriteFile.mock.calls).toEqual([['./foo.js']]); }); - test('with --ignore-path to ignore files matching patterns from the working directories .ignorePath', () => { + test('with --ignore-path to ignore files matching patterns from the working directories .ignorePath', async () => { const onWriteFile = jest.fn(); mockGitFs('', { '/sub-directory/.ignorePath': '*.md', }); - prettyQuick('/sub-directory/', { + await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile, ignorePath: '/.ignorePath', diff --git a/src/__tests__/scm-hg.test.js b/src/__tests__/scm-hg.test.js index 61dd3bc..a22fe0f 100644 --- a/src/__tests__/scm-hg.test.js +++ b/src/__tests__/scm-hg.test.js @@ -22,7 +22,7 @@ const mockHgFs = (additionalFiles = {}) => { additionalFiles, ), ); - execa.sync.mockImplementation((command, args) => { + execa.mockImplementation(async (command, args) => { if (command !== 'hg') { throw new Error(`unexpected command: ${command}`); } @@ -42,91 +42,95 @@ const mockHgFs = (additionalFiles = {}) => { }; describe('with hg', () => { - test('calls `hg debugancestor`', () => { + test('calls `hg debugancestor`', async () => { mock({ '/.hg': {}, }); - prettyQuick('root'); + await prettyQuick('/'); - expect(execa.sync).toHaveBeenCalledWith( + expect(execa).toHaveBeenCalledWith( 'hg', ['debugancestor', 'tip', 'default'], { cwd: '/' }, ); }); - test('calls `hg debugancestor` with root hg directory', () => { + test('calls `hg debugancestor` with root hg directory', async () => { mock({ '/.hg': {}, '/other-dir': {}, }); - prettyQuick('/other-dir'); - expect(execa.sync).toHaveBeenCalledWith( + await prettyQuick('/other-dir'); + expect(execa).toHaveBeenCalledWith( 'hg', ['debugancestor', 'tip', 'default'], { cwd: '/' }, ); }); - test('calls `hg status` with revision', () => { + test('calls `hg status` with revision', async () => { mock({ '/.hg': {}, }); - prettyQuick('root', { since: 'banana' }); + await prettyQuick('/', { since: 'banana' }); - expect(execa.sync).toHaveBeenCalledWith( + expect(execa).toHaveBeenCalledWith( 'hg', ['status', '-n', '-a', '-m', '--rev', 'banana'], { cwd: '/' }, ); }); - test('calls onFoundSinceRevision with return value from `hg debugancestor`', () => { + test('calls onFoundSinceRevision with return value from `hg debugancestor`', async () => { const onFoundSinceRevision = jest.fn(); mock({ '/.hg': {}, }); - execa.sync.mockReturnValue({ stdout: 'banana' }); + execa.mockReturnValue(Promise.resolve({ stdout: 'banana' })); - prettyQuick('root', { onFoundSinceRevision }); + await prettyQuick('/', { onFoundSinceRevision }); expect(onFoundSinceRevision).toHaveBeenCalledWith('hg', 'banana'); }); - test('calls onFoundChangedFiles with changed files', () => { + test('calls onFoundChangedFiles with changed files', async () => { const onFoundChangedFiles = jest.fn(); mockHgFs(); - prettyQuick('root', { since: 'banana', onFoundChangedFiles }); + await prettyQuick('/', { since: 'banana', onFoundChangedFiles }); expect(onFoundChangedFiles).toHaveBeenCalledWith(['./foo.js', './bar.md']); }); - test('calls onWriteFile with changed files', () => { + test('calls onWriteFile with changed files', async () => { const onWriteFile = jest.fn(); mockHgFs(); - prettyQuick('root', { since: 'banana', onWriteFile }); + await prettyQuick('/', { since: 'banana', onWriteFile }); expect(onWriteFile).toHaveBeenCalledWith('./foo.js'); expect(onWriteFile).toHaveBeenCalledWith('./bar.md'); }); - test('calls onWriteFile with changed files for the given pattern', () => { + test('calls onWriteFile with changed files for the given pattern', async () => { const onWriteFile = jest.fn(); mockHgFs(); - prettyQuick('root', { pattern: '*.md', since: 'banana', onWriteFile }); + await prettyQuick('/', { + pattern: '*.md', + since: 'banana', + onWriteFile, + }); expect(onWriteFile.mock.calls).toEqual([['./bar.md']]); }); - test('calls onWriteFile with changed files for the given globstar pattern', () => { + test('calls onWriteFile with changed files for the given globstar pattern', async () => { const onWriteFile = jest.fn(); mockHgFs(); - prettyQuick('root', { + await prettyQuick('/', { pattern: '**/*.md', since: 'banana', onWriteFile, @@ -134,10 +138,10 @@ describe('with hg', () => { expect(onWriteFile.mock.calls).toEqual([['./bar.md']]); }); - test('calls onWriteFile with changed files for the given extglob pattern', () => { + test('calls onWriteFile with changed files for the given extglob pattern', async () => { const onWriteFile = jest.fn(); mockHgFs(); - prettyQuick('root', { + await prettyQuick('/', { pattern: '*.*(md|foo|bar)', since: 'banana', onWriteFile, @@ -145,37 +149,37 @@ describe('with hg', () => { expect(onWriteFile.mock.calls).toEqual([['./bar.md']]); }); - test('writes formatted files to disk', () => { + test('writes formatted files to disk', async () => { const onWriteFile = jest.fn(); mockHgFs(); - prettyQuick('root', { since: 'banana', onWriteFile }); + await prettyQuick('/', { since: 'banana', onWriteFile }); expect(fs.readFileSync('/foo.js', 'utf8')).toEqual('formatted:foo()'); expect(fs.readFileSync('/bar.md', 'utf8')).toEqual('formatted:# foo'); }); - test('succeeds if a file was changed and bail is not set', () => { + test('succeeds if a file was changed and bail is not set', async () => { mockHgFs(); - const result = prettyQuick('root', { since: 'banana' }); + const result = await prettyQuick('/', { since: 'banana' }); expect(result).toEqual({ errors: [], success: true }); }); - test('fails if a file was changed and bail is set to true', () => { + test('fails if a file was changed and bail is set to true', async () => { mockHgFs(); - const result = prettyQuick('root', { since: 'banana', bail: true }); + const result = await prettyQuick('/', { since: 'banana', bail: true }); expect(result).toEqual({ errors: ['BAIL_ON_WRITE'], success: false }); }); - test('calls onWriteFile with changed files for an array of globstar patterns', () => { + test('calls onWriteFile with changed files for an array of globstar patterns', async () => { const onWriteFile = jest.fn(); mockHgFs(); - prettyQuick('root', { + await prettyQuick('/', { pattern: ['**/*.foo', '**/*.md', '**/*.bar'], since: 'banana', onWriteFile, @@ -183,61 +187,66 @@ describe('with hg', () => { expect(onWriteFile.mock.calls).toEqual([['./bar.md']]); }); - test('without --staged does NOT stage changed files', () => { + test('without --staged does NOT stage changed files', async () => { mockHgFs(); - prettyQuick('root', { since: 'banana' }); + await prettyQuick('/', { since: 'banana' }); - expect(execa.sync).not.toHaveBeenCalledWith('hg', ['add', './foo.js'], { + expect(execa).not.toHaveBeenCalledWith('hg', ['add', './foo.js'], { cwd: '/', }); - expect(execa.sync).not.toHaveBeenCalledWith('hg', ['add', './bar.md'], { + expect(execa).not.toHaveBeenCalledWith('hg', ['add', './bar.md'], { cwd: '/', }); }); - test('with --verbose calls onExamineFile', () => { + test('with --verbose calls onExamineFile', async () => { const onExamineFile = jest.fn(); mockHgFs(); - prettyQuick('root', { since: 'banana', verbose: true, onExamineFile }); + await prettyQuick('/', { + since: 'banana', + verbose: true, + onExamineFile, + }); expect(onExamineFile).toHaveBeenCalledWith('./foo.js'); expect(onExamineFile).toHaveBeenCalledWith('./bar.md'); }); - test('without --verbose does NOT call onExamineFile', () => { + test('without --verbose does NOT call onExamineFile', async () => { const onExamineFile = jest.fn(); mockHgFs(); - prettyQuick('root', { since: 'banana', onExamineFile }); + await prettyQuick('/', { since: 'banana', onExamineFile }); expect(onExamineFile).not.toHaveBeenCalledWith('./foo.js'); expect(onExamineFile).not.toHaveBeenCalledWith('./bar.md'); }); - test('ignore files matching patterns from the repositories root .prettierignore', () => { + test('ignore files matching patterns from the repositories root .prettierignore', async () => { const onWriteFile = jest.fn(); mockHgFs({ '/.prettierignore': '*.md', }); - prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }); + await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }); expect(onWriteFile.mock.calls).toEqual([['./foo.js']]); }); - test('ignore files matching patterns from the working directories .prettierignore', () => { + test('ignore files matching patterns from the working directories .prettierignore', async () => { const onWriteFile = jest.fn(); mockHgFs({ '/sub-directory/.prettierignore': '*.md', }); - prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }); + + await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile }); expect(onWriteFile.mock.calls).toEqual([['./foo.js']]); }); - test('with --ignore-path to ignore files matching patterns from the repositories root .ignorePath', () => { + test('with --ignore-path to ignore files matching patterns from the repositories root .ignorePath', async () => { const onWriteFile = jest.fn(); mockHgFs({ '/.ignorePath': '*.md', }); - prettyQuick('/sub-directory/', { + await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile, ignorePath: '/.ignorePath', @@ -245,12 +254,12 @@ describe('with hg', () => { expect(onWriteFile.mock.calls).toEqual([['./foo.js']]); }); - test('with --ignore-path to ignore files matching patterns from the working directories .ignorePath', () => { + test('with --ignore-path to ignore files matching patterns from the working directories .ignorePath', async () => { const onWriteFile = jest.fn(); mockHgFs({ '/.ignorePath': '*.md', }); - prettyQuick('/sub-directory/', { + await prettyQuick('/sub-directory/', { since: 'banana', onWriteFile, ignorePath: '/.ignorePath', diff --git a/src/createIgnorer.js b/src/createIgnorer.js index ab7866e..65f889a 100644 --- a/src/createIgnorer.js +++ b/src/createIgnorer.js @@ -1,11 +1,11 @@ -import { existsSync, readFileSync } from 'fs'; +import * as fs from 'fs'; import { join } from 'path'; import ignore from 'ignore'; -export default (directory, filename = '.prettierignore') => { +export default async (directory, filename = '.prettierignore') => { const file = join(directory, filename); - if (existsSync(file)) { - const text = readFileSync(file, 'utf8'); + if (fs.existsSync(file)) { + const text = await fs.promises.readFile(file, 'utf8'); const filter = ignore().add(text).createFilter(); return (path) => filter(join(path)); } diff --git a/src/index.js b/src/index.js index 8d5b5af..506bb95 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ import createIgnorer from './createIgnorer'; import createMatcher from './createMatcher'; import isSupportedExtension from './isSupportedExtension'; -export default ( +export default async ( currentDirectory, { config, @@ -23,35 +23,38 @@ export default ( onExamineFile, onCheckFile, onWriteFile, + onStageFiles, resolveConfig = true, } = {}, ) => { - const scm = scms(currentDirectory); + const scm = await scms(currentDirectory); if (!scm) { throw new Error('Unable to detect a source control manager.'); } const directory = scm.rootDirectory; - const revision = since || scm.getSinceRevision(directory, { staged, branch }); + const revision = + since || (await scm.getSinceRevision(directory, { staged, branch })); onFoundSinceRevision && onFoundSinceRevision(scm.name, revision); - - const rootIgnorer = createIgnorer(directory, ignorePath); + const rootIgnorer = await createIgnorer(directory, ignorePath); const cwdIgnorer = currentDirectory !== directory - ? createIgnorer(currentDirectory, ignorePath) + ? await createIgnorer(currentDirectory, ignorePath) : () => true; + const rawChangedFiles = await scm.getChangedFiles( + directory, + revision, + staged, + ); - const changedFiles = scm - .getChangedFiles(directory, revision, staged) + const changedFiles = rawChangedFiles .filter(createMatcher(pattern)) .filter(rootIgnorer) .filter(cwdIgnorer) .filter(isSupportedExtension(resolveConfig)); - const unstagedFiles = staged - ? scm - .getUnstagedChangedFiles(directory, revision) + ? (await scm.getUnstagedChangedFiles(directory, revision)) .filter(isSupportedExtension) .filter(createMatcher(pattern)) .filter(rootIgnorer) @@ -64,24 +67,26 @@ export default ( const failReasons = new Set(); - processFiles(directory, changedFiles, { + const filesToStage = []; + await processFiles(directory, changedFiles, { check, config, - onWriteFile: (file) => { + onWriteFile: async (file) => { onWriteFile && onWriteFile(file); + if (bail) { failReasons.add('BAIL_ON_WRITE'); } if (staged && restage) { if (wasFullyStaged(file)) { - scm.stageFile(directory, file); + filesToStage.push(file); } else { onPartiallyStagedFile && onPartiallyStagedFile(file); failReasons.add('PARTIALLY_STAGED_FILE'); } } }, - onCheckFile: (file, isFormatted) => { + onCheckFile: async (file, isFormatted) => { onCheckFile && onCheckFile(file, isFormatted); if (!isFormatted) { failReasons.add('CHECK_FAILED'); @@ -90,6 +95,15 @@ export default ( onExamineFile: verbose && onExamineFile, }); + if (filesToStage.length > 0) { + try { + onStageFiles && onStageFiles(); + await scm.stageFiles(directory, filesToStage); + } catch (e) { + failReasons.add('STAGE_FAILED'); + } + } + return { success: failReasons.size === 0, errors: Array.from(failReasons), diff --git a/src/processFiles.js b/src/processFiles.js index 07d8302..d4fc6bb 100644 --- a/src/processFiles.js +++ b/src/processFiles.js @@ -1,36 +1,44 @@ -import { readFileSync, writeFileSync } from 'fs'; +import * as fs from 'fs'; import * as prettier from 'prettier'; import { join } from 'path'; -export default ( +export default async ( directory, files, { check, config, onExamineFile, onCheckFile, onWriteFile } = {}, ) => { + const promises = []; for (const relative of files) { - onExamineFile && onExamineFile(relative); - const file = join(directory, relative); - const options = Object.assign( - {}, - prettier.resolveConfig.sync(file, { - config, - editorconfig: true, - }), - { filepath: file }, - ); - const input = readFileSync(file, 'utf8'); + promises.push( + (async () => { + onExamineFile && (await onExamineFile(relative)); + const file = join(directory, relative); + + const options = Object.assign( + {}, + await prettier.resolveConfig(file, { + config, + editorconfig: true, + }), + { filepath: file }, + ); - if (check) { - const isFormatted = prettier.check(input, options); - onCheckFile && onCheckFile(relative, isFormatted); - continue; - } + const input = await fs.promises.readFile(file, 'utf8'); - const output = prettier.format(input, options); + if (check) { + const isFormatted = prettier.check(input, options); + onCheckFile && (await onCheckFile(relative, isFormatted)); + return; + } - if (output !== input) { - writeFileSync(file, output); - onWriteFile && onWriteFile(relative); - } + const output = prettier.format(input, options); + + if (output !== input) { + await fs.promises.writeFile(file, output); + onWriteFile && (await onWriteFile(relative)); + } + })(), + ); } + await Promise.all(promises); }; diff --git a/src/scms/git.js b/src/scms/git.js index b5ac5e9..0bc7b31 100644 --- a/src/scms/git.js +++ b/src/scms/git.js @@ -5,12 +5,12 @@ import * as fs from 'fs'; export const name = 'git'; -export const detect = (directory) => { +export const detect = async (directory) => { if (fs.existsSync(join(directory, '.git'))) { return directory; } - const gitDirectory = findUp.sync('.git', { + const gitDirectory = await findUp('.git', { cwd: directory, type: 'directory', }); @@ -18,7 +18,7 @@ export const detect = (directory) => { return dirname(gitDirectory); } - const gitWorktreeFile = findUp.sync('.git', { + const gitWorktreeFile = await findUp('.git', { cwd: directory, type: 'file', }); @@ -28,23 +28,23 @@ export const detect = (directory) => { } }; -const runGit = (directory, args) => - execa.sync('git', args, { +const runGit = async (directory, args) => + await execa('git', args, { cwd: directory, }); const getLines = (execaResult) => execaResult.stdout.split('\n'); -export const getSinceRevision = (directory, { staged, branch }) => { +export const getSinceRevision = async (directory, { staged, branch }) => { try { const revision = staged ? 'HEAD' - : runGit(directory, [ - 'merge-base', - 'HEAD', - branch || 'master', - ]).stdout.trim(); - return runGit(directory, ['rev-parse', '--short', revision]).stdout.trim(); + : ( + await runGit(directory, ['merge-base', 'HEAD', branch || 'master']) + ).stdout.trim(); + return ( + await runGit(directory, ['rev-parse', '--short', revision]) + ).stdout.trim(); } catch (error) { if ( /HEAD/.test(error.message) || @@ -56,10 +56,10 @@ export const getSinceRevision = (directory, { staged, branch }) => { } }; -export const getChangedFiles = (directory, revision, staged) => { +export const getChangedFiles = async (directory, revision, staged) => { return [ ...getLines( - runGit( + await runGit( directory, [ 'diff', @@ -73,15 +73,34 @@ export const getChangedFiles = (directory, revision, staged) => { ...(staged ? [] : getLines( - runGit(directory, ['ls-files', '--others', '--exclude-standard']), + await runGit(directory, [ + 'ls-files', + '--others', + '--exclude-standard', + ]), )), ].filter(Boolean); }; -export const getUnstagedChangedFiles = (directory) => { - return getChangedFiles(directory, null, false); +export const getUnstagedChangedFiles = async (directory) => { + return await getChangedFiles(directory, null, false); }; -export const stageFile = (directory, file) => { - runGit(directory, ['add', file]); +export const stageFiles = async (directory, files) => { + const maxArguments = 100; + const result = files.reduce((resultArray, file, index) => { + const chunkIndex = Math.floor(index / maxArguments); + + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = []; // start a new chunk + } + + resultArray[chunkIndex].push(file); + + return resultArray; + }, []); + + for (let batchedFiles of result) { + await runGit(directory, ['add', ...batchedFiles]); + } }; diff --git a/src/scms/hg.js b/src/scms/hg.js index 2f7141e..c8903ee 100644 --- a/src/scms/hg.js +++ b/src/scms/hg.js @@ -1,11 +1,16 @@ import findUp from 'find-up'; import execa from 'execa'; -import { dirname } from 'path'; +import { dirname, join } from 'path'; +import * as fs from 'fs'; export const name = 'hg'; -export const detect = (directory) => { - const hgDirectory = findUp.sync('.hg', { +export const detect = async (directory) => { + if (fs.existsSync(join(directory, '.hg'))) { + return directory; + } + + const hgDirectory = await findUp('.hg', { cwd: directory, type: 'directory', }); @@ -14,26 +19,24 @@ export const detect = (directory) => { } }; -const runHg = (directory, args) => - execa.sync('hg', args, { +const runHg = async (directory, args) => + await execa('hg', args, { cwd: directory, }); const getLines = (execaResult) => execaResult.stdout.split('\n'); -export const getSinceRevision = (directory, { branch }) => { - const revision = runHg(directory, [ - 'debugancestor', - 'tip', - branch || 'default', - ]).stdout.trim(); - return runHg(directory, ['id', '-i', '-r', revision]).stdout.trim(); +export const getSinceRevision = async (directory, { branch }) => { + const revision = ( + await runHg(directory, ['debugancestor', 'tip', branch || 'default']) + ).stdout.trim(); + return (await runHg(directory, ['id', '-i', '-r', revision])).stdout.trim(); }; -export const getChangedFiles = (directory, revision) => { +export const getChangedFiles = async (directory, revision) => { return [ ...getLines( - runHg(directory, ['status', '-n', '-a', '-m', '--rev', revision]), + await runHg(directory, ['status', '-n', '-a', '-m', '--rev', revision]), ), ].filter(Boolean); }; @@ -42,6 +45,21 @@ export const getUnstagedChangedFiles = () => { return []; }; -export const stageFile = (directory, file) => { - runHg(directory, ['add', file]); +export const stageFiles = async (directory, files) => { + const maxArguments = 100; + const result = files.reduce((resultArray, file, index) => { + const chunkIndex = Math.floor(index / maxArguments); + + if (!resultArray[chunkIndex]) { + resultArray[chunkIndex] = []; // start a new chunk + } + + resultArray[chunkIndex].push(file); + + return resultArray; + }, []); + + for (let batchedFiles of result) { + await runHg(directory, ['add', ...batchedFiles]); + } }; diff --git a/src/scms/index.js b/src/scms/index.js index 9fd9616..05e7fe5 100644 --- a/src/scms/index.js +++ b/src/scms/index.js @@ -3,9 +3,9 @@ import * as hgScm from './hg'; const scms = [gitScm, hgScm]; -export default (directory) => { +export default async (directory) => { for (const scm of scms) { - const rootDirectory = scm.detect(directory); + const rootDirectory = await scm.detect(directory); if (rootDirectory) { return Object.assign({ rootDirectory }, scm); }