From d594ec48bc8ed09d8a81e8b7b07ce2a41b47bd0e Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Mon, 21 Aug 2017 22:33:37 +0200 Subject: [PATCH] fix(core): determine git root correctly in sub directories (#64) * test(cli): add failing integration test * fix: determine git root correctly from sub directories * fixup! make git initializing work fixes #62 --- @commitlint/cli/cli.test.js | 102 ++++++++++++++---- .../fixtures/husky/integration/package.json | 1 + @commitlint/cli/package.json | 5 + @commitlint/core/package.json | 2 +- @commitlint/core/src/read.js | 34 ++++-- 5 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 @commitlint/cli/fixtures/husky/integration/package.json diff --git a/@commitlint/cli/cli.test.js b/@commitlint/cli/cli.test.js index 448d9c6318..2f9fdbc20c 100644 --- a/@commitlint/cli/cli.test.js +++ b/@commitlint/cli/cli.test.js @@ -1,73 +1,135 @@ import path from 'path'; import test from 'ava'; import execa from 'execa'; +import {sync as bin} from 'resolve-bin'; +import * as sander from 'sander'; import stream from 'string-to-stream'; +import tmp from 'tmp'; const here = path.join.bind(null, __dirname); +const fix = here.bind(null, 'fixtures'); -const SIMPLE = here('fixtures/simple'); -const EXTENDS_ROOT = here('fixtures/extends-root'); -const EMPTY = here('fixtures/empty'); - -const cli = (input = '', args = [], opts = {}) => { - const c = execa(here('cli.js'), args, { - capture: ['stdout'], - cwd: opts.cwd - }); - stream(input).pipe(c.stdin); - return c; +const CLI = here('cli.js'); +const SIMPLE = fix('simple'); +const EXTENDS_ROOT = fix('extends-root'); +const EMPTY = fix('empty'); + +const HUSKY = tmp.dirSync().name; +const HUSKY_INTEGRATION = path.join(tmp.dirSync().name, 'integration'); + +const exec = (command, args = [], opts = {}) => { + return async (input = '') => { + const c = execa(command, args, { + capture: ['stdout'], + cwd: opts.cwd + }); + stream(input).pipe(c.stdin); + const result = await c; + if (result.code !== 0) { + console.log(result.stderr); + } + return result; + } }; +const cli = exec.bind(null, CLI); +const git = exec.bind(null, 'git'); +const mkdir = exec.bind(null, bin('mkdirp')); +const npm = exec.bind(null, 'npm'); +const rm = exec.bind(null, bin('rimraf')); + test('should throw when called without [input]', t => { - t.throws(cli(), /Expected a raw commit/); + t.throws(cli()(), /Expected a raw commit/); }); test('should reprint input from stdin', async t => { - const actual = await cli('foo: bar', [], {cwd: EMPTY}); + const actual = await cli([], {cwd: EMPTY})('foo: bar'); t.true(actual.stdout.includes('foo: bar')); }); test('should produce no success output with --quiet flag', async t => { - const actual = await cli('foo: bar', ['--quiet'], {cwd: EMPTY}); + const actual = await cli(['--quiet'], {cwd: EMPTY})('foo: bar'); t.is(actual.stdout, ''); t.is(actual.stderr, ''); }); test('should produce no success output with -q flag', async t => { - const actual = await cli('foo: bar', ['-q'], {cwd: EMPTY}); + const actual = await cli(['-q'], {cwd: EMPTY})('foo: bar'); t.is(actual.stdout, ''); t.is(actual.stderr, ''); }); test('should succeed for input from stdin without rules', async t => { - const actual = await cli('foo: bar', [], {cwd: EMPTY}); + const actual = await cli([], {cwd: EMPTY})('foo: bar'); t.is(actual.code, 0); }); test('should fail for input from stdin with rule from rc', async t => { - const actual = await t.throws(cli('foo: bar', [], {cwd: SIMPLE})); + const actual = await t.throws(cli([], {cwd: SIMPLE})('foo: bar')); t.true(actual.stdout.includes('type must not be one of [foo]')); t.is(actual.code, 1); }); test('should fail for input from stdin with rule from js', async t => { const actual = await t.throws( - cli('foo: bar', ['--extends', './extended'], {cwd: EXTENDS_ROOT}) + cli(['--extends', './extended'], {cwd: EXTENDS_ROOT})('foo: bar') ); t.true(actual.stdout.includes('type must not be one of [foo]')); t.is(actual.code, 1); }); test('should produce no error output with --quiet flag', async t => { - const actual = await t.throws(cli('foo: bar', ['--quiet'], {cwd: SIMPLE})); + const actual = await t.throws(cli(['--quiet'], {cwd: SIMPLE})('foo: bar')); t.is(actual.stdout, ''); t.is(actual.stderr, ''); t.is(actual.code, 1); }); test('should produce no error output with -q flag', async t => { - const actual = await t.throws(cli('foo: bar', ['-q'], {cwd: SIMPLE})); + const actual = await t.throws(cli(['-q'], {cwd: SIMPLE})('foo: bar')); t.is(actual.stdout, ''); t.is(actual.stderr, ''); t.is(actual.code, 1); }); + +test('should work with husky commitmsg hook', async () => { + const cwd = HUSKY; + + await init(cwd); + await pkg(cwd); + + await npm(['install', 'husky'], {cwd})(); + await git(['add', 'package.json'], {cwd})(); + await git(['commit', '-m', '"chore: this should work"'], {cwd})(); + + await rm([HUSKY])(); +}); + +test('should work with husky commitmsg hook in sub packages', async () => { + const cwd = HUSKY_INTEGRATION; + const upper = path.dirname(HUSKY_INTEGRATION); + + await mkdir([cwd])(); + await init(upper); + await pkg(cwd); + + await npm(['install', 'husky'], {cwd})(); + await git(['add', 'package.json'], {cwd})(); + + await git(['commit', '-m', '"chore: this should work"'], {cwd})(); + + await rm([upper])(); +}); + +async function init(cwd) { + await git(['init'], {cwd})(); + + return Promise.all([ + git(['config', 'user.email', '"commitlint@gitub.com"'], {cwd})(), + git(['config', 'user.name', '"commitlint"'], {cwd})() + ]); +} + +function pkg(cwd) { + return sander.writeFile(cwd, 'package.json', JSON.stringify({scripts: {commitmsg: `${CLI} -e`}})); +} diff --git a/@commitlint/cli/fixtures/husky/integration/package.json b/@commitlint/cli/fixtures/husky/integration/package.json new file mode 100644 index 0000000000..d4019a8cc8 --- /dev/null +++ b/@commitlint/cli/fixtures/husky/integration/package.json @@ -0,0 +1 @@ +{"scripts":{"commitmsg":"commitlint -e"}} \ No newline at end of file diff --git a/@commitlint/cli/package.json b/@commitlint/cli/package.json index ac26028001..4eed3e03d0 100644 --- a/@commitlint/cli/package.json +++ b/@commitlint/cli/package.json @@ -44,7 +44,12 @@ "ava": "^0.18.2", "dependency-check": "^2.9.1", "execa": "^0.7.0", + "mkdirp": "^0.5.1", + "resolve-bin": "^0.4.0", + "rimraf": "^2.6.1", + "sander": "^0.6.0", "string-to-stream": "^1.1.0", + "tmp": "0.0.33", "xo": "^0.18.2" }, "dependencies": { diff --git a/@commitlint/core/package.json b/@commitlint/core/package.json index fbac4a8d0d..e70ce43e24 100644 --- a/@commitlint/core/package.json +++ b/@commitlint/core/package.json @@ -134,9 +134,9 @@ "chalk": "^2.0.1", "conventional-changelog-angular": "^1.3.3", "conventional-commits-parser": "^1.3.0", + "find-up": "^2.1.0", "franc": "^2.0.0", "git-raw-commits": "^1.1.2", - "git-toplevel": "^1.1.1", "import-from": "^2.1.0", "lodash": "^4.17.4", "mz": "^2.6.0", diff --git a/@commitlint/core/src/read.js b/@commitlint/core/src/read.js index f529db97b7..b62aed7026 100644 --- a/@commitlint/core/src/read.js +++ b/@commitlint/core/src/read.js @@ -1,7 +1,7 @@ -import {join} from 'path'; +import path from 'path'; import exists from 'path-exists'; +import up from 'find-up'; import gitRawCommits from 'git-raw-commits'; -import gitToplevel from 'git-toplevel'; import {readFile} from 'mz/fs'; export default getCommitMessages; @@ -45,16 +45,38 @@ function getHistoryCommits(options) { // Check if the current repository is shallow // () => Promise async function isShallow() { - const top = await gitToplevel(); - const shallow = join(top, '.git/shallow'); + const top = await toplevel(); + + if (typeof top !== 'string') { + throw new TypeError(`Could not find git root - is this a git repository?`); + } + + const shallow = path.join(top, '.git/shallow'); return exists(shallow); } // Get recently edited commit message // () => Promise> async function getEditCommit() { - const top = await gitToplevel(); - const editFilePath = join(top, '.git/COMMIT_EDITMSG'); + const top = await toplevel(); + + if (typeof top !== 'string') { + throw new TypeError(`Could not find git root - is this a git repository?`); + } + + const editFilePath = path.join(top, '.git/COMMIT_EDITMSG'); const editFile = await readFile(editFilePath); return [`${editFile.toString('utf-8')}\n`]; } + +// Find the next git root +// (start: string) => Promise +async function toplevel(cwd = process.cwd()) { + const found = await up('.git', {cwd}); + + if (typeof found !== 'string') { + return found; + } + + return path.join(found, '..'); +}