diff --git a/README.md b/README.md index 1a1060a..fd896a1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Runs [Prettier](https://prettier.io) on your changed files. Supported source control managers: * Git -* _Add more_ +* Mercurial ## Install @@ -54,6 +54,8 @@ With `npm`: You can run `pretty-quick` as a pre-commit hook using [`husky`](https://github.com/typicode/husky). +> For Mercurial have a look at [`husky-hg`](https://github.com/TobiasTimm/husky-hg) + ```shellstream yarn add --dev husky ``` diff --git a/package.json b/package.json index 4f0dd1c..e8a247d 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,17 @@ "main": "./dist", "bin": "./bin/pretty-quick.js", "license": "MIT", + "keywords": [ + "git", + "mercurial", + "hg", + "prettier", + "pretty-quick", + "formatting", + "code", + "vcs", + "precommit" + ], "files": [ "bin", "dist", diff --git a/src/__tests__/scm-hg.test.js b/src/__tests__/scm-hg.test.js new file mode 100644 index 0000000..d01e3dc --- /dev/null +++ b/src/__tests__/scm-hg.test.js @@ -0,0 +1,137 @@ +import mock from 'mock-fs'; +import execa from 'execa'; +import fs from 'fs'; + +import prettyQuick from '..'; + +jest.mock('execa'); + +afterEach(() => { + mock.restore(); + jest.clearAllMocks(); +}); + +const mockHgFs = () => { + mock({ + '/.hg': {}, + '/foo.js': 'foo()', + '/bar.md': '# foo', + }); + execa.sync.mockImplementation((command, args) => { + if (command !== 'hg') { + throw new Error(`unexpected command: ${command}`); + } + switch (args[0]) { + case 'status': + return { stdout: './foo.js\n' + './bar.md\n' }; + case 'diff': + return { stdout: './foo.js\n' + './bar.md\n' }; + case 'add': + return { stdout: '' }; + case 'log': + return { stdout: '' }; + default: + throw new Error(`unexpected arg0: ${args[0]}`); + } + }); +}; + +describe('with hg', () => { + test('calls `hg debugancestor`', () => { + mock({ + '/.hg': {}, + }); + + prettyQuick('root'); + + expect(execa.sync).toHaveBeenCalledWith( + 'hg', + ['debugancestor', 'tip', 'default'], + { cwd: '/' } + ); + }); + + test('calls `hg debugancestor` with root hg directory', () => { + mock({ + '/.hg': {}, + '/other-dir': {}, + }); + + prettyQuick('/other-dir'); + expect(execa.sync).toHaveBeenCalledWith( + 'hg', + ['debugancestor', 'tip', 'default'], + { cwd: '/' } + ); + }); + + test('calls `hg status` with revision', () => { + mock({ + '/.hg': {}, + }); + + prettyQuick('root', { since: 'banana' }); + + expect(execa.sync).toHaveBeenCalledWith( + 'hg', + ['status', '-n', '-a', '-m', '--rev', 'banana'], + { cwd: '/' } + ); + }); + + test('calls onFoundSinceRevision with return value from `hg debugancestor`', () => { + const onFoundSinceRevision = jest.fn(); + + mock({ + '/.hg': {}, + }); + execa.sync.mockReturnValue({ stdout: 'banana' }); + + prettyQuick('root', { onFoundSinceRevision }); + + expect(onFoundSinceRevision).toHaveBeenCalledWith('hg', 'banana'); + }); + + test('calls onFoundChangedFiles with changed files', () => { + const onFoundChangedFiles = jest.fn(); + mockHgFs(); + + prettyQuick('root', { since: 'banana', onFoundChangedFiles }); + + expect(onFoundChangedFiles).toHaveBeenCalledWith(['./foo.js', './bar.md']); + }); + + test('calls onWriteFile with changed files', () => { + const onWriteFile = jest.fn(); + mockHgFs(); + + prettyQuick('root', { since: 'banana', onWriteFile }); + + expect(onWriteFile).toHaveBeenCalledWith('./foo.js'); + expect(onWriteFile).toHaveBeenCalledWith('./bar.md'); + }); + + test('writes formatted files to disk', () => { + const onWriteFile = jest.fn(); + + mockHgFs(); + + prettyQuick('root', { since: 'banana', onWriteFile }); + + expect(fs.readFileSync('/foo.js', 'utf8')).toEqual('formatted:foo()'); + expect(fs.readFileSync('/bar.md', 'utf8')).toEqual('formatted:# foo'); + }); + + test('without --staged does NOT stage changed files', () => { + mockHgFs(); + + prettyQuick('root', { since: 'banana' }); + + expect(execa.sync).not.toHaveBeenCalledWith('hg', ['add', './foo.js'], { + cwd: '/', + }); + expect(execa.sync).not.toHaveBeenCalledWith('hg', ['add', './bar.md'], { + cwd: '/', + }); + }); +}); diff --git a/src/scms/hg.js b/src/scms/hg.js new file mode 100644 index 0000000..37a074c --- /dev/null +++ b/src/scms/hg.js @@ -0,0 +1,40 @@ +import findUp from 'find-up'; +import execa from 'execa'; +import { dirname } from 'path'; + +export const name = 'hg'; + +export const detect = directory => { + const hgDirectory = findUp.sync('.hg', { cwd: directory }); + if (hgDirectory) { + return dirname(hgDirectory); + } +}; + +const runHg = (directory, args) => + execa.sync('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 getChangedFiles = (directory, revision) => { + return [ + ...getLines( + runHg(directory, ['status', '-n', '-a', '-m', '--rev', revision]) + ), + ].filter(Boolean); +}; + +export const stageFile = (directory, file) => { + runHg(directory, ['add', file]); +}; diff --git a/src/scms/index.js b/src/scms/index.js index abcb6d2..cfc6f39 100644 --- a/src/scms/index.js +++ b/src/scms/index.js @@ -1,6 +1,7 @@ import * as gitScm from './git'; +import * as hgScm from './hg'; -const scms = [gitScm]; +const scms = [gitScm, hgScm]; export default directory => { for (const scm of scms) {