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 cb4bc8bc84..fc54c02496 100644 --- a/@commitlint/cli/cli.test.js +++ b/@commitlint/cli/cli.test.js @@ -137,8 +137,8 @@ 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})( 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/overriden-legacy/commitlint.config.js b/@commitlint/core/fixtures/overriden-legacy/commitlint.config.js deleted file mode 100644 index 0625ee6360..0000000000 --- a/@commitlint/core/fixtures/overriden-legacy/commitlint.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - rules: { - legacy: false - } -}; 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 ef565701ee..7fcd93fb19 100644 --- a/@commitlint/core/package.json +++ b/@commitlint/core/package.json @@ -63,7 +63,6 @@ "license": "MIT", "devDependencies": { "@commitlint/utils": "^3.1.1", - "ansi-styles": "3.1.0", "ava": "0.22.0", "babel-cli": "^6.26.0", "babel-preset-commitlint": "^3.2.0", @@ -73,15 +72,16 @@ "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", "resolve-from": "3.0.0", + "rimraf": "2.6.1", "xo": "0.18.2" }, "dependencies": { - "@marionebl/sander": "^0.6.1", + "@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", @@ -89,11 +89,9 @@ "cosmiconfig": "^3.0.1", "find-up": "^2.1.0", "franc": "^2.0.0", - "git-raw-commits": "^1.1.2", "lodash": "^4.17.4", "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/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/load.js b/@commitlint/core/src/load.js index ec4cfbb691..b8680cb250 100644 --- a/@commitlint/core/src/load.js +++ b/@commitlint/core/src/load.js @@ -1,35 +1,35 @@ import path from 'path'; -import {entries, merge, mergeWith, pick} from 'lodash'; -import rc from 'rc'; import cosmiconfig from 'cosmiconfig'; +import {entries, merge, mergeWith, pick} from 'lodash'; import resolveFrom from 'resolve-from'; -import up from 'find-up'; -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 = await 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) }; } @@ -37,7 +37,7 @@ 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 }); @@ -80,49 +80,3 @@ export default async (seed = {}) => { return registry; }, preset); }; - -async function file() { - const legacy = rc('conventional-changelog-lint'); - const legacyFound = typeof legacy.config === 'string'; - const explorer = cosmiconfig('commitlint', { - rcExtensions: true, - stopDir: await toplevel() - }); - const config = await explorer.load('.'); - - if (legacyFound && !config) { - console.warn( - `Using legacy ${path.relative( - process.cwd(), - legacy.config - )}. Rename to commitlint.config.js to silence this warning.` - ); - } - - if (legacyFound && config) { - 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 (config) { - return config.config; - } - - return legacy; -} - -// 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/load.test.js b/@commitlint/core/src/load.test.js index 99f598a3a2..7c51f1790b 100644 --- a/@commitlint/core/src/load.test.js +++ b/@commitlint/core/src/load.test.js @@ -1,53 +1,28 @@ -import {tmpdir} from 'os'; -import crypto from 'crypto'; -import path from 'path'; import test from 'ava'; -import exists from 'path-exists'; -import execa from 'execa'; -import * as sander from '@marionebl/sander'; +import {bootstrap} from './test-git'; import load from './load'; -test.beforeEach(async t => { - t.context.repo = await initRepository(); -}); - -test.afterEach.always(async t => { - await cleanRepository(t.context.repo); -}); - -test.serial('extends-empty should have no rules', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/extends-empty')) - .to(repo.directory); - - const actual = await load(); +test('extends-empty should have no rules', async t => { + const cwd = await bootstrap('fixtures/extends-empty'); + const actual = await load({}, {cwd}); t.deepEqual(actual.rules, {}); }); -test.serial('uses seed as configured', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/extends-empty')) - .to(repo.directory); - - const actual = await load({rules: {foo: 'bar'}}); +test('uses seed as configured', async t => { + const cwd = await bootstrap('fixtures/extends-empty'); + const actual = await load({rules: {foo: 'bar'}}, {cwd}); t.is(actual.rules.foo, 'bar'); }); -test.serial('uses seed with parserPreset', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/parser-preset')) - .to(repo.directory); - - const {parserPreset: actual} = await load({ - parserPreset: './conventional-changelog-custom' - }); +test('uses seed with parserPreset', async t => { + const cwd = await bootstrap('fixtures/parser-preset'); + const {parserPreset: actual} = await load( + { + parserPreset: './conventional-changelog-custom' + }, + {cwd} + ); t.is(actual.name, './conventional-changelog-custom'); t.deepEqual(actual.opts, { @@ -57,46 +32,38 @@ test.serial('uses seed with parserPreset', async t => { }); }); -test.serial('invalid extend should throw', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/extends-invalid')) - .to(repo.directory); - - await t.throws(load()); +test('invalid extend should throw', async t => { + const cwd = await bootstrap('fixtures/extends-invalid'); + await t.throws(load({}, {cwd})); }); -test.serial('empty file should have no rules', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/empty-object-file')) - .to(repo.directory); - - const actual = await load(); +test('empty file should have no rules', async t => { + const cwd = await bootstrap('fixtures/empty-object-file'); + const actual = await load({}, {cwd}); t.deepEqual(actual.rules, {}); }); -test.serial('empty file should extend nothing', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/empty-file')) - .to(repo.directory); - - const actual = await load(); +test('empty file should extend nothing', async t => { + const cwd = await bootstrap('fixtures/empty-file'); + const actual = await load({}, {cwd}); t.deepEqual(actual.extends, []); }); -test.serial('recursive extends', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/recursive-extends')) - .to(repo.directory); +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 + } + }); +}); - const actual = await load(); +test('recursive extends', async t => { + const cwd = await bootstrap('fixtures/recursive-extends'); + const actual = await load({}, {cwd}); t.deepEqual(actual, { extends: ['./first-extended'], rules: { @@ -107,14 +74,10 @@ test.serial('recursive extends', async t => { }); }); -test.serial('recursive extends with json file', async t => { - const {repo} = t.context; +test('recursive extends with json file', async t => { + const cwd = await bootstrap('fixtures/recursive-extends-json'); + const actual = await load({}, {cwd}); - await sander - .copydir(path.join(repo.previous, 'fixtures/recursive-extends-json')) - .to(repo.directory); - - const actual = await load(); t.deepEqual(actual, { extends: ['./first-extended'], rules: { @@ -125,14 +88,10 @@ test.serial('recursive extends with json file', async t => { }); }); -test.serial('recursive extends with yaml file', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/recursive-extends-yaml')) - .to(repo.directory); +test('recursive extends with yaml file', async t => { + const cwd = await bootstrap('fixtures/recursive-extends-yaml'); + const actual = await load({}, {cwd}); - const actual = await load(); t.deepEqual(actual, { extends: ['./first-extended'], rules: { @@ -143,14 +102,10 @@ test.serial('recursive extends with yaml file', async t => { }); }); -test.serial('recursive extends with js file', async t => { - const {repo} = t.context; +test('recursive extends with js file', async t => { + const cwd = await bootstrap('fixtures/recursive-extends-js'); + const actual = await load({}, {cwd}); - await sander - .copydir(path.join(repo.previous, 'fixtures/recursive-extends-js')) - .to(repo.directory); - - const actual = await load(); t.deepEqual(actual, { extends: ['./first-extended'], rules: { @@ -161,14 +116,10 @@ test.serial('recursive extends with js file', async t => { }); }); -test.serial('recursive extends with package.json file', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/recursive-extends-package')) - .to(repo.directory); +test('recursive extends with package.json file', async t => { + const cwd = await bootstrap('fixtures/recursive-extends-package'); + const actual = await load({}, {cwd}); - const actual = await load(); t.deepEqual(actual, { extends: ['./first-extended'], rules: { @@ -179,39 +130,23 @@ test.serial('recursive extends with package.json file', async t => { }); }); -test.serial( - 'parser preset overwrites completely instead of merging', - async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/parser-preset-override')) - .to(repo.directory); - const actual = await load(); +test('parser preset overwrites completely instead of merging', async t => { + 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'); - t.deepEqual(actual.parserPreset.opts, { - b: 'b', - parserOpts: { - headerPattern: /.*/ - } - }); - } -); - -test.serial('recursive extends with parserPreset', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/recursive-parser-preset')) - .to(repo.directory); - - await sander - .copydir(path.join(repo.previous, 'node_modules')) - .to(path.join('node_modules')); + t.is(actual.parserPreset.name, './custom'); + t.is(typeof actual.parserPreset.opts, 'object'); + t.deepEqual(actual.parserPreset.opts, { + b: 'b', + parserOpts: { + headerPattern: /.*/ + } + }); +}); - const actual = await load(); +test('recursive extends with parserPreset', async t => { + 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'); @@ -221,14 +156,10 @@ test.serial('recursive extends with parserPreset', async t => { ); }); -test.serial('ignores unknow keys', async t => { - const {repo} = t.context; +test('ignores unknow keys', async t => { + const cwd = await bootstrap('fixtures/trash-file'); + const actual = await load({}, {cwd}); - await sander - .copydir(path.join(repo.previous, 'fixtures/trash-file')) - .to(repo.directory); - - const actual = await load(); t.deepEqual(actual, { extends: [], rules: { @@ -238,14 +169,10 @@ test.serial('ignores unknow keys', async t => { }); }); -test.serial('ignores unknow keys recursively', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/trash-extend')) - .to(repo.directory); +test('ignores unknow keys recursively', async t => { + const cwd = await bootstrap('fixtures/trash-extend'); + const actual = await load({}, {cwd}); - const actual = await load(); t.deepEqual(actual, { extends: ['./one'], rules: { @@ -254,69 +181,3 @@ test.serial('ignores unknow keys recursively', async t => { } }); }); - -test.serial('supports legacy .conventional-changelog-lintrc', async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/legacy')) - .to(repo.directory); - - const actual = await load(); - t.deepEqual(actual, { - extends: [], - rules: { - legacy: true - } - }); -}); - -test.serial( - 'commitlint.config.js overrides .conventional-changelog-lintrc', - async t => { - const {repo} = t.context; - - await sander - .copydir(path.join(repo.previous, 'fixtures/overriden-legacy')) - .to(repo.directory); - - const actual = await load(); - t.deepEqual(actual, { - extends: [], - rules: { - legacy: false - } - }); - } -); - -async function initRepository() { - const previous = process.cwd(); - const directory = path.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 cleanRepository(repo) { - if (repo.previous && repo.previous !== process.cwd()) { - process.chdir(repo.previous); - } - - if (await exists(repo.directory)) { - await sander.rimraf(repo.directory); - } -} - -function rand() { - return crypto - .randomBytes(Math.ceil(6)) - .toString('hex') - .slice(0, 12); -} diff --git a/@commitlint/core/src/read.js b/@commitlint/core/src/read.js index 44f902802e..09cf812f4f 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 '@marionebl/sander'; +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 e4d6d14543..d38b3ff56d 100644 --- a/@commitlint/core/src/read.test.js +++ b/@commitlint/core/src/read.test.js @@ -1,115 +1,53 @@ -import {tmpdir} from 'os'; -import crypto from 'crypto'; -import {join} from 'path'; - import test from 'ava'; import execa from 'execa'; -import exists from 'path-exists'; import * as sander from '@marionebl/sander'; import pkg from '../package'; +import {bootstrap, clone} from './test-git'; import read from './read'; -test.beforeEach(async t => { - t.context.repos = [await initRepository()]; -}); +test('get edit commit message from git root', async t => { + const cwd = await bootstrap(); -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.serial('get edit commit message from git root', async t => { - await sander.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 sander.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 sander.mkdir('beta'); - await sander.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; +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})); - await sander.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]; - - 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 sander.rimraf(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..0810f12367 --- /dev/null +++ b/@commitlint/core/src/test-git.js @@ -0,0 +1,42 @@ +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 Promise.all([ + execa('git', ['config', 'user.name', 'ava'], {cwd}), + execa('git', ['config', 'user.email', 'test@example.com'], {cwd}) + ]); + + return cwd; +} + +async function clone(source, ...args) { + const cwd = path.join(os.tmpdir(), rand()); + await execa('git', ['clone', ...args, source, cwd]); + await execa('git', ['config', 'user.email', 'test@example.com'], {cwd}); + await execa('git', ['config', 'user.name', 'ava'], {cwd}); + return cwd; +} + +function rand() { + return crypto + .randomBytes(Math.ceil(6)) + .toString('hex') + .slice(0, 12); +} 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" } }