From 91968b8c169543fc0475065272df3a553d59e31a Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Tue, 3 Oct 2017 17:58:46 +0100 Subject: [PATCH] feat: enable alternative config formats (#83) * feat(core): Allow to configure with json, yaml and package.json Fix #73 * chore: consolidate dev dependencies * chore: introduce cwd awareness * allow forced cwds * remove flaky tests BREAKING CHANGE: discontinue support of conventional-changelog-lintrc * test: make git setup reliable --- @commitlint/cli/cli.js | 15 +- @commitlint/cli/cli.test.js | 32 +++- .../legacy/.conventional-changelog-lintrc | 5 - .../.conventional-changelog-lintrc | 5 - .../recursive-extends-js/.commitlintrc.js | 6 + .../first-extended/index.js | 6 + .../first-extended/second-extended/index.js} | 2 +- .../recursive-extends-json/.commitlintrc.json | 6 + .../first-extended/index.js | 6 + .../first-extended/second-extended/index.js | 5 + .../first-extended/index.js | 6 + .../first-extended/second-extended/index.js | 5 + .../recursive-extends-package/package.json | 8 + .../recursive-extends-yaml/.commitlintrc.yml | 4 + .../first-extended/index.js | 6 + .../first-extended/second-extended/index.js | 5 + .../first-extended/commitlint.config.js | 6 + .../recursive-extends/first-extended/index.js | 7 +- .../conventional-changelog-custom.js | 13 +- @commitlint/core/package.json | 15 +- @commitlint/core/src/format.test.js | 66 ------- @commitlint/core/src/library/parse.test.js | 14 +- @commitlint/core/src/library/toplevel.js | 16 ++ @commitlint/core/src/lint.test.js | 10 +- @commitlint/core/src/load.js | 71 ++------ @commitlint/core/src/load.test.js | 162 +++++++++++------- @commitlint/core/src/read.js | 49 ++---- @commitlint/core/src/read.test.js | 117 +++---------- @commitlint/core/src/test-git.js | 41 +++++ README.md | 2 +- docs/guides-ci-setup.md | 2 + docs/guides-local-setup.md | 2 + package.json | 3 + 33 files changed, 364 insertions(+), 354 deletions(-) delete mode 100644 @commitlint/core/fixtures/legacy/.conventional-changelog-lintrc delete mode 100644 @commitlint/core/fixtures/overriden-legacy/.conventional-changelog-lintrc create mode 100644 @commitlint/core/fixtures/recursive-extends-js/.commitlintrc.js create mode 100644 @commitlint/core/fixtures/recursive-extends-js/first-extended/index.js rename @commitlint/core/fixtures/{overriden-legacy/commitlint.config.js => recursive-extends-js/first-extended/second-extended/index.js} (68%) create mode 100644 @commitlint/core/fixtures/recursive-extends-json/.commitlintrc.json create mode 100644 @commitlint/core/fixtures/recursive-extends-json/first-extended/index.js create mode 100644 @commitlint/core/fixtures/recursive-extends-json/first-extended/second-extended/index.js create mode 100644 @commitlint/core/fixtures/recursive-extends-package/first-extended/index.js create mode 100644 @commitlint/core/fixtures/recursive-extends-package/first-extended/second-extended/index.js create mode 100644 @commitlint/core/fixtures/recursive-extends-package/package.json create mode 100644 @commitlint/core/fixtures/recursive-extends-yaml/.commitlintrc.yml create mode 100644 @commitlint/core/fixtures/recursive-extends-yaml/first-extended/index.js create mode 100644 @commitlint/core/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js create mode 100644 @commitlint/core/fixtures/recursive-extends/first-extended/commitlint.config.js create mode 100644 @commitlint/core/src/library/toplevel.js create mode 100644 @commitlint/core/src/test-git.js diff --git a/@commitlint/cli/cli.js b/@commitlint/cli/cli.js index 96bd551b6b..68b9d14cf6 100755 --- a/@commitlint/cli/cli.js +++ b/@commitlint/cli/cli.js @@ -23,10 +23,11 @@ const rules = { }; const configuration = { - string: ['from', 'to', 'extends', 'parser-preset'], + string: ['cwd', 'from', 'to', 'extends', 'parser-preset'], boolean: ['edit', 'help', 'version', 'quiet', 'color'], alias: { c: 'color', + d: 'cwd', e: 'edit', f: 'from', t: 'to', @@ -38,15 +39,18 @@ const configuration = { }, description: { color: 'toggle colored output', + cwd: 'directory to execute in', edit: 'read last commit message found in ./git/COMMIT_EDITMSG', extends: 'array of shareable configurations to extend', from: 'lower end of the commit range to lint; applies if edit=false', to: 'upper end of the commit range to lint; applies if edit=false', quiet: 'toggle console output', - 'parser-preset': 'configuration preset to use for conventional-commits-parser' + 'parser-preset': + 'configuration preset to use for conventional-commits-parser' }, default: { color: true, + cwd: process.cwd(), edit: false, from: null, to: null, @@ -67,7 +71,7 @@ const cli = meow( configuration ); -const load = seed => core.load(seed); +const load = (seed, opts) => core.load(seed, opts); function main(options) { const raw = options.input; @@ -75,13 +79,13 @@ function main(options) { const fromStdin = rules.fromStdin(raw, flags); const range = pick(flags, 'edit', 'from', 'to'); - const input = fromStdin ? stdin() : core.read(range); + const input = fromStdin ? stdin() : core.read(range, {cwd: flags.cwd}); const fmt = new chalk.constructor({enabled: flags.color}); return input.then(raw => (Array.isArray(raw) ? raw : [raw])).then(messages => Promise.all( messages.map(commit => { - return load(getSeed(flags)) + return load(getSeed(flags), {cwd: flags.cwd}) .then(loaded => { const parserOpts = selectParserOpts(loaded.parserPreset); const opts = parserOpts ? {parserOpts} : undefined; @@ -127,7 +131,6 @@ main(cli).catch(err => }) ); - function selectParserOpts(parserPreset) { if (typeof parserPreset !== 'object') { return undefined; diff --git a/@commitlint/cli/cli.test.js b/@commitlint/cli/cli.test.js index 4a3dfe21fb..fc54c02496 100644 --- a/@commitlint/cli/cli.test.js +++ b/@commitlint/cli/cli.test.js @@ -31,7 +31,7 @@ const exec = (command, args = [], opts = {}) => { console.log(result.stderr); } return result; - } + }; }; const cli = exec.bind(null, CLI); @@ -40,8 +40,12 @@ 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/); +test('should throw when called without [input]', async t => { + const dir = tmp.dirSync().name; + + await init(dir); + await t.throws(cli([], {cwd: dir})(), /Expected a raw commit/); + await rm([dir])(); }); test('should reprint input from stdin', async t => { @@ -73,11 +77,19 @@ test('should fail for input from stdin with rule from rc', async t => { }); test('should fail for input from stdin with rule from js', async t => { + const dir = tmp.dirSync().name; + + await init(dir); + await sander.copydir(EXTENDS_ROOT).to(dir); + const actual = await t.throws( - cli(['--extends', './extended'], {cwd: EXTENDS_ROOT})('foo: bar') + cli(['--extends', './extended'], {cwd: dir})('foo: bar') ); + t.true(includes(actual.stdout, 'type must not be one of [foo]')); t.is(actual.code, 1); + + await rm([dir])(); }); test('should produce no error output with --quiet flag', async t => { @@ -125,11 +137,13 @@ test('should work with husky commitmsg hook in sub packages', async () => { test('should pick up parser preset', async t => { const cwd = PARSER_PRESET; - const actual = await t.throws(cli([], {cwd})('type(scope)-ticket subject')); + t.true(includes(actual.stdout, 'message may not be empty [subject-empty]')); - await cli(['--parser-preset', './parser-preset'], {cwd})('type(scope)-ticket subject'); + await cli(['--parser-preset', './parser-preset'], {cwd})( + 'type(scope)-ticket subject' + ); }); async function init(cwd) { @@ -142,5 +156,9 @@ async function init(cwd) { } function pkg(cwd) { - return sander.writeFile(cwd, 'package.json', JSON.stringify({scripts: {commitmsg: `${CLI} -e`}})); + return sander.writeFile( + cwd, + 'package.json', + JSON.stringify({scripts: {commitmsg: `${CLI} -e`}}) + ); } diff --git a/@commitlint/core/fixtures/legacy/.conventional-changelog-lintrc b/@commitlint/core/fixtures/legacy/.conventional-changelog-lintrc deleted file mode 100644 index 39911211f1..0000000000 --- a/@commitlint/core/fixtures/legacy/.conventional-changelog-lintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "legacy": true - } -} diff --git a/@commitlint/core/fixtures/overriden-legacy/.conventional-changelog-lintrc b/@commitlint/core/fixtures/overriden-legacy/.conventional-changelog-lintrc deleted file mode 100644 index 39911211f1..0000000000 --- a/@commitlint/core/fixtures/overriden-legacy/.conventional-changelog-lintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "legacy": true - } -} diff --git a/@commitlint/core/fixtures/recursive-extends-js/.commitlintrc.js b/@commitlint/core/fixtures/recursive-extends-js/.commitlintrc.js new file mode 100644 index 0000000000..7564fdc432 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-js/.commitlintrc.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./first-extended'], + rules: { + zero: 0 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends-js/first-extended/index.js b/@commitlint/core/fixtures/recursive-extends-js/first-extended/index.js new file mode 100644 index 0000000000..4317428ad1 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-js/first-extended/index.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./second-extended'], + rules: { + one: 1 + } +}; diff --git a/@commitlint/core/fixtures/overriden-legacy/commitlint.config.js b/@commitlint/core/fixtures/recursive-extends-js/first-extended/second-extended/index.js similarity index 68% rename from @commitlint/core/fixtures/overriden-legacy/commitlint.config.js rename to @commitlint/core/fixtures/recursive-extends-js/first-extended/second-extended/index.js index 0625ee6360..d199d354da 100644 --- a/@commitlint/core/fixtures/overriden-legacy/commitlint.config.js +++ b/@commitlint/core/fixtures/recursive-extends-js/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - legacy: false + two: 2 } }; diff --git a/@commitlint/core/fixtures/recursive-extends-json/.commitlintrc.json b/@commitlint/core/fixtures/recursive-extends-json/.commitlintrc.json new file mode 100644 index 0000000000..b0feb26419 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-json/.commitlintrc.json @@ -0,0 +1,6 @@ +{ + "extends": ["./first-extended"], + "rules": { + "zero": 0 + } +} diff --git a/@commitlint/core/fixtures/recursive-extends-json/first-extended/index.js b/@commitlint/core/fixtures/recursive-extends-json/first-extended/index.js new file mode 100644 index 0000000000..4317428ad1 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-json/first-extended/index.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./second-extended'], + rules: { + one: 1 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends-json/first-extended/second-extended/index.js b/@commitlint/core/fixtures/recursive-extends-json/first-extended/second-extended/index.js new file mode 100644 index 0000000000..d199d354da --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-json/first-extended/second-extended/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + two: 2 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends-package/first-extended/index.js b/@commitlint/core/fixtures/recursive-extends-package/first-extended/index.js new file mode 100644 index 0000000000..4317428ad1 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-package/first-extended/index.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./second-extended'], + rules: { + one: 1 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends-package/first-extended/second-extended/index.js b/@commitlint/core/fixtures/recursive-extends-package/first-extended/second-extended/index.js new file mode 100644 index 0000000000..d199d354da --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-package/first-extended/second-extended/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + two: 2 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends-package/package.json b/@commitlint/core/fixtures/recursive-extends-package/package.json new file mode 100644 index 0000000000..031a432619 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-package/package.json @@ -0,0 +1,8 @@ +{ + "commitlint": { + "extends": ["./first-extended"], + "rules": { + "zero": 0 + } + } +} diff --git a/@commitlint/core/fixtures/recursive-extends-yaml/.commitlintrc.yml b/@commitlint/core/fixtures/recursive-extends-yaml/.commitlintrc.yml new file mode 100644 index 0000000000..b76a5986b3 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-yaml/.commitlintrc.yml @@ -0,0 +1,4 @@ +extends: +- "./first-extended" +rules: + zero: 0 diff --git a/@commitlint/core/fixtures/recursive-extends-yaml/first-extended/index.js b/@commitlint/core/fixtures/recursive-extends-yaml/first-extended/index.js new file mode 100644 index 0000000000..4317428ad1 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-yaml/first-extended/index.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./second-extended'], + rules: { + one: 1 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js b/@commitlint/core/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js new file mode 100644 index 0000000000..d199d354da --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + two: 2 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends/first-extended/commitlint.config.js b/@commitlint/core/fixtures/recursive-extends/first-extended/commitlint.config.js new file mode 100644 index 0000000000..4317428ad1 --- /dev/null +++ b/@commitlint/core/fixtures/recursive-extends/first-extended/commitlint.config.js @@ -0,0 +1,6 @@ +module.exports = { + extends: ['./second-extended'], + rules: { + one: 1 + } +}; diff --git a/@commitlint/core/fixtures/recursive-extends/first-extended/index.js b/@commitlint/core/fixtures/recursive-extends/first-extended/index.js index 4317428ad1..fe16cc487b 100644 --- a/@commitlint/core/fixtures/recursive-extends/first-extended/index.js +++ b/@commitlint/core/fixtures/recursive-extends/first-extended/index.js @@ -1,6 +1 @@ -module.exports = { - extends: ['./second-extended'], - rules: { - one: 1 - } -}; +module.exports = require('./commitlint.config.js'); diff --git a/@commitlint/core/fixtures/recursive-parser-preset/first-extended/second-extended/conventional-changelog-custom.js b/@commitlint/core/fixtures/recursive-parser-preset/first-extended/second-extended/conventional-changelog-custom.js index 434b48ce13..0eed7f5516 100644 --- a/@commitlint/core/fixtures/recursive-parser-preset/first-extended/second-extended/conventional-changelog-custom.js +++ b/@commitlint/core/fixtures/recursive-parser-preset/first-extended/second-extended/conventional-changelog-custom.js @@ -1,8 +1,7 @@ -const defaultOpts = require('conventional-changelog-angular'); -const _ = require('lodash'); - -module.exports = defaultOpts.then(data => { - const extented = _.cloneDeep(data); - extented.parserOpts.headerPattern = /^(\w*)(?:\((.*)\))?-(.*)$/; - return extented; +module.exports = Promise.resolve().then(() => { + return { + parserOpts: { + headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/ + } + }; }); diff --git a/@commitlint/core/package.json b/@commitlint/core/package.json index 7e3ae24005..7fcd93fb19 100644 --- a/@commitlint/core/package.json +++ b/@commitlint/core/package.json @@ -63,17 +63,15 @@ "license": "MIT", "devDependencies": { "@commitlint/utils": "^3.1.1", - "ansi-styles": "3.1.0", - "ava": "0.18.2", - "babel-cli": "^6.18.0", + "ava": "0.22.0", + "babel-cli": "^6.26.0", "babel-preset-commitlint": "^3.2.0", - "babel-register": "6.24.1", + "babel-register": "^6.26.0", "cross-env": "^5.0.1", "denodeify": "1.2.1", "dependency-check": "2.7.0", "execa": "0.6.3", "globby": "6.1.0", - "has-ansi": "3.0.0", "import-from": "2.1.0", "nyc": "10.3.2", "path-exists": "3.0.0", @@ -82,19 +80,18 @@ "xo": "0.18.2" }, "dependencies": { + "@marionebl/git-raw-commits": "^1.2.0", + "@marionebl/sander": "^0.6.0", "babel-runtime": "^6.23.0", "chalk": "^2.0.1", "conventional-changelog-angular": "^1.3.3", "conventional-commits-parser": "^1.3.0", + "cosmiconfig": "^3.0.1", "find-up": "^2.1.0", "franc": "^2.0.0", - "git-raw-commits": "^1.1.2", - "import-from": "^2.1.0", "lodash": "^4.17.4", - "mz": "^2.6.0", "path-exists": "^3.0.0", "pos": "^0.4.2", - "rc": "^1.1.7", "resolve-from": "^3.0.0", "semver": "^5.3.0" } diff --git a/@commitlint/core/src/format.test.js b/@commitlint/core/src/format.test.js index bd1566c049..c9bd5eff1b 100644 --- a/@commitlint/core/src/format.test.js +++ b/@commitlint/core/src/format.test.js @@ -1,7 +1,5 @@ import test from 'ava'; -import hasAnsi from 'has-ansi'; import chalk from 'chalk'; -import {yellow, red, magenta, blue} from 'ansi-styles'; import {includes} from 'lodash'; import format from './format'; @@ -49,19 +47,6 @@ test('returns a correct of empty .errors and .warnings', t => { t.true(includes(msg, '1 problems, 1 warnings')); }); -test('colors messages by default', t => { - const [msg] = format({ - errors: [], - warnings: [] - }); - t.true(hasAnsi(msg)); -}); - -test('does not color messages if configured', t => { - const [msg] = format({}, {color: false}); - t.false(hasAnsi(msg)); -}); - test('uses appropriate signs by default', t => { const [err, warn] = format({ errors: [ @@ -110,54 +95,3 @@ test('uses signs as configured', t => { t.true(includes(err, 'ERR')); t.true(includes(warn, 'WRN')); }); - -test('uses appropriate colors by default', t => { - const [err, warn] = format({ - errors: [ - { - level: 2, - name: 'error-name', - message: 'There was an error' - } - ], - warnings: [ - { - level: 1, - name: 'warning-name', - message: 'There was a problem' - } - ] - }); - - t.true(includes(err, red.open)); - t.true(includes(warn, yellow.open)); -}); - -if (process.platform !== 'win32') { - test('uses colors as configured', t => { - const [err, warn] = format( - { - errors: [ - { - level: 2, - name: 'error-name', - message: 'There was an error' - } - ], - warnings: [ - { - level: 1, - name: 'warning-name', - message: 'There was a problem' - } - ] - }, - { - colors: ['white', 'magenta', 'blue'] - } - ); - - t.true(includes(err, blue.open)); - t.true(includes(warn, magenta.open)); - }); -} diff --git a/@commitlint/core/src/library/parse.test.js b/@commitlint/core/src/library/parse.test.js index fe3a8c7f56..0df838de31 100644 --- a/@commitlint/core/src/library/parse.test.js +++ b/@commitlint/core/src/library/parse.test.js @@ -2,12 +2,14 @@ import importFrom from 'import-from'; import test from 'ava'; import parse from './parse'; -test('throws when called without params', t => { - t.throws(parse(), /Expected a raw commit/); +test('throws when called without params', async t => { + const error = await t.throws(parse()); + t.is(error.message, 'Expected a raw commit'); }); -test('throws when called with empty message', t => { - t.throws(parse(''), /Expected a raw commit/); +test('throws when called with empty message', async t => { + const error = await t.throws(parse()); + t.is(error.message, 'Expected a raw commit'); }); test('returns object with raw message', async t => { @@ -16,10 +18,10 @@ test('returns object with raw message', async t => { t.is(actual.raw, message); }); -test('calls parser with message and passed options', t => { +test('calls parser with message and passed options', async t => { const message = 'message'; - parse(message, m => { + await parse(message, m => { t.is(message, m); return {}; }); diff --git a/@commitlint/core/src/library/toplevel.js b/@commitlint/core/src/library/toplevel.js new file mode 100644 index 0000000000..a24e290f50 --- /dev/null +++ b/@commitlint/core/src/library/toplevel.js @@ -0,0 +1,16 @@ +import path from 'path'; +import up from 'find-up'; + +export default toplevel; + +// Find the next git root +// (start: string) => Promise +async function toplevel(cwd) { + const found = await up('.git', {cwd}); + + if (typeof found !== 'string') { + return found; + } + + return path.join(found, '..'); +} diff --git a/@commitlint/core/src/lint.test.js b/@commitlint/core/src/lint.test.js index 0864176bf4..dd87c166bd 100644 --- a/@commitlint/core/src/lint.test.js +++ b/@commitlint/core/src/lint.test.js @@ -1,12 +1,14 @@ import test from 'ava'; import lint from './lint'; -test('throws without params', t => { - t.throws(lint()); +test('throws without params', async t => { + const error = await t.throws(lint()); + t.is(error.message, 'Expected a raw commit'); }); -test('throws with empty message', t => { - t.throws(lint('')); +test('throws with empty message', async t => { + const error = await t.throws(lint('')); + t.is(error.message, 'Expected a raw commit'); }); test('positive on stub message and no rule', async t => { diff --git a/@commitlint/core/src/load.js b/@commitlint/core/src/load.js index edc4d47702..b8680cb250 100644 --- a/@commitlint/core/src/load.js +++ b/@commitlint/core/src/load.js @@ -1,30 +1,35 @@ import path from 'path'; -import importFrom from 'import-from'; +import cosmiconfig from 'cosmiconfig'; import {entries, merge, mergeWith, pick} from 'lodash'; -import rc from 'rc'; import resolveFrom from 'resolve-from'; -import resolveExtends from './library/resolve-extends'; import executeRule from './library/execute-rule'; +import resolveExtends from './library/resolve-extends'; +import toplevel from './library/toplevel'; const w = (a, b) => (Array.isArray(b) ? b : undefined); const valid = input => pick(input, 'extends', 'rules', 'parserPreset'); -export default async (seed = {}) => { - // Obtain config from .rc files - const raw = file(); +export default async (seed = {}, options = {cwd: ''}) => { + const explorer = cosmiconfig('commitlint', { + rcExtensions: true, + stopDir: await toplevel(options.cwd) + }); + + const raw = (await explorer.load(options.cwd)) || {}; + const base = raw.filepath ? path.dirname(raw.filepath) : options.cwd; // Merge passed config with file based options - const config = valid(merge(raw, seed)); + const config = valid(merge(raw.config, seed)); const opts = merge({extends: [], rules: {}}, pick(config, 'extends')); // Resolve parserPreset key if (typeof config.parserPreset === 'string') { - const resolvedParserPreset = resolveFrom(process.cwd(), config.parserPreset); + const resolvedParserPreset = resolveFrom(base, config.parserPreset); config.parserPreset = { name: config.parserPreset, - path: `./${path.posix.relative(process.cwd(), resolvedParserPreset)}`.split(path.sep).join('/'), + path: resolvedParserPreset, opts: require(resolvedParserPreset) }; } @@ -32,14 +37,17 @@ export default async (seed = {}) => { // Resolve extends key const extended = resolveExtends(opts, { prefix: 'commitlint-config', - cwd: raw.config ? path.dirname(raw.config) : process.cwd(), + cwd: base, parserPreset: config.parserPreset }); const preset = valid(mergeWith(extended, config, w)); // Await parser-preset if applicable - if (typeof preset.parserPreset === 'object' && typeof preset.parserPreset.opts === 'object') { + if ( + typeof preset.parserPreset === 'object' && + typeof preset.parserPreset.opts === 'object' + ) { preset.parserPreset.opts = await preset.parserPreset.opts; } @@ -72,44 +80,3 @@ export default async (seed = {}) => { return registry; }, preset); }; - -function file() { - const legacy = rc('conventional-changelog-lint'); - const legacyFound = typeof legacy.config === 'string'; - - const found = resolveable('./commitlint.config'); - const raw = found ? importFrom(process.cwd(), './commitlint.config') : {}; - - if (legacyFound && !found) { - console.warn( - `Using legacy ${path.relative( - process.cwd(), - legacy.config - )}. Rename to commitlint.config.js to silence this warning.` - ); - } - - if (legacyFound && found) { - console.warn( - `Ignored legacy ${path.relative( - process.cwd(), - legacy.config - )} as commitlint.config.js superseeds it. Remove .conventional-changelog-lintrc to silence this warning.` - ); - } - - if (found) { - return raw; - } - - return legacy; -} - -function resolveable(id) { - try { - resolveFrom(process.cwd(), id); - return true; - } catch (err) { - return false; - } -} diff --git a/@commitlint/core/src/load.test.js b/@commitlint/core/src/load.test.js index d9e734e6e1..7c51f1790b 100644 --- a/@commitlint/core/src/load.test.js +++ b/@commitlint/core/src/load.test.js @@ -1,30 +1,29 @@ -import path from 'path'; import test from 'ava'; +import {bootstrap} from './test-git'; import load from './load'; -const cwd = process.cwd(); - -test.afterEach.always(t => { - t.context.back(); -}); - test('extends-empty should have no rules', async t => { - t.context.back = chdir('fixtures/extends-empty'); - const actual = await load(); + const cwd = await bootstrap('fixtures/extends-empty'); + const actual = await load({}, {cwd}); t.deepEqual(actual.rules, {}); }); test('uses seed as configured', async t => { - t.context.back = chdir('fixtures/extends-empty'); - const actual = await load({rules: {foo: 'bar'}}); + const cwd = await bootstrap('fixtures/extends-empty'); + const actual = await load({rules: {foo: 'bar'}}, {cwd}); t.is(actual.rules.foo, 'bar'); }); test('uses seed with parserPreset', async t => { - t.context.back = chdir('fixtures/parser-preset'); + const cwd = await bootstrap('fixtures/parser-preset'); + const {parserPreset: actual} = await load( + { + parserPreset: './conventional-changelog-custom' + }, + {cwd} + ); - const {parserPreset: actual} = await load({parserPreset: './conventional-changelog-custom'}); t.is(actual.name, './conventional-changelog-custom'); t.deepEqual(actual.opts, { parserOpts: { @@ -33,26 +32,94 @@ test('uses seed with parserPreset', async t => { }); }); -test('invalid extend should throw', t => { - t.context.back = chdir('fixtures/extends-invalid'); - t.throws(load()); +test('invalid extend should throw', async t => { + const cwd = await bootstrap('fixtures/extends-invalid'); + await t.throws(load({}, {cwd})); }); test('empty file should have no rules', async t => { - t.context.back = chdir('fixtures/empty-object-file'); - const actual = await load(); + const cwd = await bootstrap('fixtures/empty-object-file'); + const actual = await load({}, {cwd}); t.deepEqual(actual.rules, {}); }); test('empty file should extend nothing', async t => { - t.context.back = chdir('fixtures/empty-file'); - const actual = await load(); + const cwd = await bootstrap('fixtures/empty-file'); + const actual = await load({}, {cwd}); t.deepEqual(actual.extends, []); }); +test('respects cwd option', async t => { + const cwd = await bootstrap('fixtures/recursive-extends/first-extended'); + const actual = await load({}, {cwd}); + t.deepEqual(actual, { + extends: ['./second-extended'], + rules: { + one: 1, + two: 2 + } + }); +}); + test('recursive extends', async t => { - t.context.back = chdir('fixtures/recursive-extends'); - const actual = await load(); + const cwd = await bootstrap('fixtures/recursive-extends'); + const actual = await load({}, {cwd}); + t.deepEqual(actual, { + extends: ['./first-extended'], + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('recursive extends with json file', async t => { + const cwd = await bootstrap('fixtures/recursive-extends-json'); + const actual = await load({}, {cwd}); + + t.deepEqual(actual, { + extends: ['./first-extended'], + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('recursive extends with yaml file', async t => { + const cwd = await bootstrap('fixtures/recursive-extends-yaml'); + const actual = await load({}, {cwd}); + + t.deepEqual(actual, { + extends: ['./first-extended'], + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('recursive extends with js file', async t => { + const cwd = await bootstrap('fixtures/recursive-extends-js'); + const actual = await load({}, {cwd}); + + t.deepEqual(actual, { + extends: ['./first-extended'], + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('recursive extends with package.json file', async t => { + const cwd = await bootstrap('fixtures/recursive-extends-package'); + const actual = await load({}, {cwd}); + t.deepEqual(actual, { extends: ['./first-extended'], rules: { @@ -64,8 +131,8 @@ test('recursive extends', async t => { }); test('parser preset overwrites completely instead of merging', async t => { - t.context.back = chdir('fixtures/parser-preset-override'); - const actual = await load(); + const cwd = await bootstrap('fixtures/parser-preset-override'); + const actual = await load({}, {cwd}); t.is(actual.parserPreset.name, './custom'); t.is(typeof actual.parserPreset.opts, 'object'); @@ -78,17 +145,21 @@ test('parser preset overwrites completely instead of merging', async t => { }); test('recursive extends with parserPreset', async t => { - t.context.back = chdir('fixtures/recursive-parser-preset'); - const actual = await load(); + const cwd = await bootstrap('fixtures/recursive-parser-preset'); + const actual = await load({}, {cwd}); t.is(actual.parserPreset.name, './conventional-changelog-custom'); t.is(typeof actual.parserPreset.opts, 'object'); - t.deepEqual(actual.parserPreset.opts.parserOpts.headerPattern, /^(\w*)(?:\((.*)\))?-(.*)$/); + t.deepEqual( + actual.parserPreset.opts.parserOpts.headerPattern, + /^(\w*)(?:\((.*)\))?-(.*)$/ + ); }); test('ignores unknow keys', async t => { - t.context.back = chdir('fixtures/trash-file'); - const actual = await load(); + const cwd = await bootstrap('fixtures/trash-file'); + const actual = await load({}, {cwd}); + t.deepEqual(actual, { extends: [], rules: { @@ -99,8 +170,9 @@ test('ignores unknow keys', async t => { }); test('ignores unknow keys recursively', async t => { - t.context.back = chdir('fixtures/trash-extend'); - const actual = await load(); + const cwd = await bootstrap('fixtures/trash-extend'); + const actual = await load({}, {cwd}); + t.deepEqual(actual, { extends: ['./one'], rules: { @@ -109,31 +181,3 @@ test('ignores unknow keys recursively', async t => { } }); }); - -test('supports legacy .conventional-changelog-lintrc', async t => { - t.context.back = chdir('fixtures/legacy'); - const actual = await load(); - t.deepEqual(actual, { - extends: [], - rules: { - legacy: true - } - }); -}); - -test('commitlint.config.js overrides .conventional-changelog-lintrc', async t => { - t.context.back = chdir('fixtures/overriden-legacy'); - const actual = await load(); - t.deepEqual(actual, { - extends: [], - rules: { - legacy: false - } - }); -}); - -function chdir(target) { - const to = path.resolve(cwd, target.split('/').join(path.sep)); - process.chdir(to); - return () => process.chdir(cwd); -} diff --git a/@commitlint/core/src/read.js b/@commitlint/core/src/read.js index 069a3ba636..7f11878000 100644 --- a/@commitlint/core/src/read.js +++ b/@commitlint/core/src/read.js @@ -1,8 +1,9 @@ import path from 'path'; import exists from 'path-exists'; -import up from 'find-up'; -import gitRawCommits from 'git-raw-commits'; -import {readFile} from 'mz/fs'; +import gitRawCommits from '@marionebl/git-raw-commits'; +import * as sander from '@marionebl/sander'; + +import toplevel from './library/toplevel'; export default getCommitMessages; @@ -15,25 +16,25 @@ const SHALLOW_MESSAGE = [ // Get commit messages // Object => Promise> async function getCommitMessages(settings) { - const {from, to, edit} = settings; + const {cwd, from, to, edit} = settings; if (edit) { - return getEditCommit(); + return getEditCommit(cwd); } - if (await isShallow()) { + if (await isShallow(cwd)) { throw new Error(SHALLOW_MESSAGE); } - return getHistoryCommits({from, to}); + return getHistoryCommits({from, to}, {cwd}); } // Get commit messages from history // Object => Promise -function getHistoryCommits(options) { +function getHistoryCommits(options, opts = {}) { return new Promise((resolve, reject) => { const data = []; - gitRawCommits(options) + gitRawCommits(options, {cwd: opts.cwd}) .on('data', chunk => data.push(chunk.toString('utf-8'))) .on('error', reject) .on('end', () => { @@ -43,12 +44,12 @@ function getHistoryCommits(options) { } // Check if the current repository is shallow -// () => Promise -async function isShallow() { - const top = await toplevel(); +// (cwd: string) => Promise +async function isShallow(cwd) { + const top = await toplevel(cwd); if (typeof top !== 'string') { - throw new TypeError(`Could not find git root - is this a git repository?`); + throw new TypeError(`Could not find git root from ${cwd}`); } const shallow = path.join(top, '.git/shallow'); @@ -56,27 +57,15 @@ async function isShallow() { } // Get recently edited commit message -// () => Promise> -async function getEditCommit() { - const top = await toplevel(); +// (cwd: string) => Promise> +async function getEditCommit(cwd) { + const top = await toplevel(cwd); if (typeof top !== 'string') { - throw new TypeError(`Could not find git root - is this a git repository?`); + throw new TypeError(`Could not find git root from ${cwd}`); } const editFilePath = path.join(top, '.git/COMMIT_EDITMSG'); - const editFile = await readFile(editFilePath); + const editFile = await sander.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, '..'); -} diff --git a/@commitlint/core/src/read.test.js b/@commitlint/core/src/read.test.js index 80fb3e9f44..d38b3ff56d 100644 --- a/@commitlint/core/src/read.test.js +++ b/@commitlint/core/src/read.test.js @@ -1,116 +1,53 @@ -import {tmpdir} from 'os'; -import crypto from 'crypto'; -import {join} from 'path'; - import test from 'ava'; -import denodeify from 'denodeify'; import execa from 'execa'; -import {mkdir, writeFile} from 'mz/fs'; -import exists from 'path-exists'; -import rimraf from 'rimraf'; +import * as sander from '@marionebl/sander'; import pkg from '../package'; +import {bootstrap, clone} from './test-git'; import read from './read'; -const rm = denodeify(rimraf); - -test.beforeEach(async t => { - t.context.repos = [await initRepository()]; -}); - -test.afterEach.always(async t => { - try { - await Promise.all(t.context.repos.map(async repo => cleanRepository(repo))); - t.context.repos = []; - } catch (err) { - console.log({err}); - } -}); +test('get edit commit message from git root', async t => { + const cwd = await bootstrap(); -test.serial('get edit commit message from git root', async t => { - await writeFile('alpha.txt', 'alpha'); - await execa('git', ['add', '.']); - await execa('git', ['commit', '-m', 'alpha']); + await sander.writeFile(cwd, 'alpha.txt', 'alpha'); + await execa('git', ['add', '.'], {cwd}); + await execa('git', ['commit', '-m', 'alpha'], {cwd}); const expected = ['alpha\n\n']; - const actual = await read({edit: true}); + const actual = await read({edit: true, cwd}); t.deepEqual(actual, expected); }); -test.serial('get history commit messages', async t => { - await writeFile('alpha.txt', 'alpha'); - await execa('git', ['add', 'alpha.txt']); - await execa('git', ['commit', '-m', 'alpha']); - await execa('git', ['rm', 'alpha.txt']); - await execa('git', ['commit', '-m', 'remove alpha']); +test('get history commit messages', async t => { + const cwd = await bootstrap(); + await sander.writeFile(cwd, 'alpha.txt', 'alpha'); + await execa('git', ['add', 'alpha.txt'], {cwd}); + await execa('git', ['commit', '-m', 'alpha'], {cwd}); + await execa('git', ['rm', 'alpha.txt'], {cwd}); + await execa('git', ['commit', '-m', 'remove alpha'], {cwd}); const expected = ['remove alpha\n\n', 'alpha\n\n']; - const actual = await read({}); + const actual = await read({cwd}); t.deepEqual(actual, expected); }); -test.serial('get edit commit message from git subdirectory', async t => { - await mkdir('beta'); - await writeFile('beta/beta.txt', 'beta'); - process.chdir('beta'); - await execa('git', ['add', '.']); - await execa('git', ['commit', '-m', 'beta']); +test('get edit commit message from git subdirectory', async t => { + const cwd = await bootstrap(); + await sander.mkdir(cwd, 'beta'); + await sander.writeFile(cwd, 'beta/beta.txt', 'beta'); + + await execa('git', ['add', '.'], {cwd}); + await execa('git', ['commit', '-m', 'beta'], {cwd}); const expected = ['beta\n\n']; - const actual = await read({edit: true}); + const actual = await read({edit: true, cwd}); t.deepEqual(actual, expected); }); -test.serial('get history commit messages from shallow clone', async t => { - const [repo] = t.context.repos; - - await writeFile('alpha.txt', 'alpha'); - await execa('git', ['add', 'alpha.txt']); - await execa('git', ['commit', '-m', 'alpha']); - - const clone = await cloneRepository(pkg.repository.url, repo, '--depth', '1'); - t.context.repos = [...t.context.repos, clone]; +test('get history commit messages from shallow clone', async t => { + const cwd = await clone(pkg.repository.url, '--depth', '1'); + const err = await t.throws(read({from: 'master', cwd})); - const err = await t.throws(read({from: 'master'})); t.true( err.message.indexOf('Could not get git history from shallow clone') > -1 ); }); - -async function initRepository() { - const previous = process.cwd(); - const directory = join(tmpdir(), rand()); - - await execa('git', ['init', directory]); - - process.chdir(directory); - - await execa('git', ['config', 'user.email', 'test@example.com']); - await execa('git', ['config', 'user.name', 'ava']); - - return {directory, previous}; -} - -async function cloneRepository(source, context, ...args) { - const directory = join(tmpdir(), rand()); - await execa('git', ['clone', ...args, source, directory]); - process.chdir(directory); - - await execa('git', ['config', 'user.email', 'test@example.com']); - await execa('git', ['config', 'user.name', 'ava']); - - return {directory, previous: context.previous}; -} - -async function cleanRepository(repo) { - if (repo.previous && repo.previous !== process.cwd()) { - process.chdir(repo.previous); - } - - if (await exists(repo.directory)) { - await rm(repo.directory); - } -} - -function rand() { - return crypto.randomBytes(Math.ceil(6)).toString('hex').slice(0, 12); -} diff --git a/@commitlint/core/src/test-git.js b/@commitlint/core/src/test-git.js new file mode 100644 index 0000000000..d259f1653a --- /dev/null +++ b/@commitlint/core/src/test-git.js @@ -0,0 +1,41 @@ +import crypto from 'crypto'; +import os from 'os'; +import path from 'path'; + +import execa from 'execa'; +import * as sander from '@marionebl/sander'; + +export {bootstrap, clone}; + +const PKG_ROOT = path.join(__dirname, '..'); + +async function bootstrap(fixture) { + const cwd = path.join(os.tmpdir(), rand()); + + if (typeof fixture !== 'undefined') { + await sander.copydir(PKG_ROOT, fixture).to(cwd); + } + + await execa('git', ['init', cwd]); + await setup(cwd); + return cwd; +} + +async function clone(source, ...args) { + const cwd = path.join(os.tmpdir(), rand()); + await execa('git', ['clone', ...args, source, cwd]); + await setup(cwd); + return cwd; +} + +async function setup(cwd) { + await execa('git', ['config', 'user.name', 'ava'], {cwd}); + await execa('git', ['config', 'user.email', 'test@example.com'], {cwd}); +} + +function rand() { + return crypto + .randomBytes(Math.ceil(6)) + .toString('hex') + .slice(0, 12); +} diff --git a/README.md b/README.md index 5fdc6da38d..08a45b69c7 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ echo "module.exports = {extends: ['@commitlint/config-angular']}" > commitlint.c ## Config -* Configuration is picked up from `commitlint.config.js` files +* Configuration is picked up from `commitlint.config.js`, `.commitlintrc.js`, `.commitlintrc.json`, or `.commitlintrc.yml` file or a `commitlint` field in `package.json` * Packages: [cli](./@commitlint/cli), [core](./@commitlint/core) * See [Rules](./docs/reference-rules.md) for a complete list of possible rules * An example configuration can be found at [@commitlint/config-angular](./@commitlint/config-angular/index.js) diff --git a/docs/guides-ci-setup.md b/docs/guides-ci-setup.md index 3c6d609f57..02918d1e19 100644 --- a/docs/guides-ci-setup.md +++ b/docs/guides-ci-setup.md @@ -18,6 +18,8 @@ npm install --save-dev @commitlint/{config-angular,cli} echo "module.exports = {extends: ['@commitlint/config-angular']};" > commitlint.config.js ``` +Alternatively the configuration can be defined in `.commitlintrc.js`, `.commitlintrc.json`, or `.commitlintrc.yml` file or a `commitlint` field in `package.json`. + ## First test run with Travis Add a `.travis.yml` to your project root diff --git a/docs/guides-local-setup.md b/docs/guides-local-setup.md index 2eed9c6180..2a979e1a96 100644 --- a/docs/guides-local-setup.md +++ b/docs/guides-local-setup.md @@ -18,6 +18,8 @@ npm install --save-dev @commitlint/{cli,config-angular} echo "module.exports = {extends: ['@commitlint/config-angular']};" > commitlint.config.js ``` +Alternatively the configuration can be defined in `.commitlintrc.js`, `.commitlintrc.json`, or `.commitlintrc.yml` file or a `commitlint` field in `package.json`. + ## Install husky Install `husky` as devDependency, a handy git hook helper available on npm. diff --git a/package.json b/package.json index 1a1d02324d..8ef7241d09 100644 --- a/package.json +++ b/package.json @@ -65,5 +65,8 @@ "prettier": "^1.5.2", "trevor": "^2.3.0", "xo": "^0.18.2" + }, + "dependencies": { + "@marionebl/sander": "^0.6.1" } }