Skip to content

Commit 91968b8

Browse files
authoredOct 3, 2017
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
1 parent 09d0494 commit 91968b8

File tree

33 files changed

+364
-354
lines changed

33 files changed

+364
-354
lines changed
 

‎@commitlint/cli/cli.js

+9-6
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ const rules = {
2323
};
2424

2525
const configuration = {
26-
string: ['from', 'to', 'extends', 'parser-preset'],
26+
string: ['cwd', 'from', 'to', 'extends', 'parser-preset'],
2727
boolean: ['edit', 'help', 'version', 'quiet', 'color'],
2828
alias: {
2929
c: 'color',
30+
d: 'cwd',
3031
e: 'edit',
3132
f: 'from',
3233
t: 'to',
@@ -38,15 +39,18 @@ const configuration = {
3839
},
3940
description: {
4041
color: 'toggle colored output',
42+
cwd: 'directory to execute in',
4143
edit: 'read last commit message found in ./git/COMMIT_EDITMSG',
4244
extends: 'array of shareable configurations to extend',
4345
from: 'lower end of the commit range to lint; applies if edit=false',
4446
to: 'upper end of the commit range to lint; applies if edit=false',
4547
quiet: 'toggle console output',
46-
'parser-preset': 'configuration preset to use for conventional-commits-parser'
48+
'parser-preset':
49+
'configuration preset to use for conventional-commits-parser'
4750
},
4851
default: {
4952
color: true,
53+
cwd: process.cwd(),
5054
edit: false,
5155
from: null,
5256
to: null,
@@ -67,21 +71,21 @@ const cli = meow(
6771
configuration
6872
);
6973

70-
const load = seed => core.load(seed);
74+
const load = (seed, opts) => core.load(seed, opts);
7175

7276
function main(options) {
7377
const raw = options.input;
7478
const flags = options.flags;
7579
const fromStdin = rules.fromStdin(raw, flags);
7680

7781
const range = pick(flags, 'edit', 'from', 'to');
78-
const input = fromStdin ? stdin() : core.read(range);
82+
const input = fromStdin ? stdin() : core.read(range, {cwd: flags.cwd});
7983
const fmt = new chalk.constructor({enabled: flags.color});
8084

8185
return input.then(raw => (Array.isArray(raw) ? raw : [raw])).then(messages =>
8286
Promise.all(
8387
messages.map(commit => {
84-
return load(getSeed(flags))
88+
return load(getSeed(flags), {cwd: flags.cwd})
8589
.then(loaded => {
8690
const parserOpts = selectParserOpts(loaded.parserPreset);
8791
const opts = parserOpts ? {parserOpts} : undefined;
@@ -127,7 +131,6 @@ main(cli).catch(err =>
127131
})
128132
);
129133

130-
131134
function selectParserOpts(parserPreset) {
132135
if (typeof parserPreset !== 'object') {
133136
return undefined;

‎@commitlint/cli/cli.test.js

+25-7
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const exec = (command, args = [], opts = {}) => {
3131
console.log(result.stderr);
3232
}
3333
return result;
34-
}
34+
};
3535
};
3636

3737
const cli = exec.bind(null, CLI);
@@ -40,8 +40,12 @@ const mkdir = exec.bind(null, bin('mkdirp'));
4040
const npm = exec.bind(null, 'npm');
4141
const rm = exec.bind(null, bin('rimraf'));
4242

43-
test('should throw when called without [input]', t => {
44-
t.throws(cli()(), /Expected a raw commit/);
43+
test('should throw when called without [input]', async t => {
44+
const dir = tmp.dirSync().name;
45+
46+
await init(dir);
47+
await t.throws(cli([], {cwd: dir})(), /Expected a raw commit/);
48+
await rm([dir])();
4549
});
4650

4751
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 => {
7377
});
7478

7579
test('should fail for input from stdin with rule from js', async t => {
80+
const dir = tmp.dirSync().name;
81+
82+
await init(dir);
83+
await sander.copydir(EXTENDS_ROOT).to(dir);
84+
7685
const actual = await t.throws(
77-
cli(['--extends', './extended'], {cwd: EXTENDS_ROOT})('foo: bar')
86+
cli(['--extends', './extended'], {cwd: dir})('foo: bar')
7887
);
88+
7989
t.true(includes(actual.stdout, 'type must not be one of [foo]'));
8090
t.is(actual.code, 1);
91+
92+
await rm([dir])();
8193
});
8294

8395
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 () => {
125137

126138
test('should pick up parser preset', async t => {
127139
const cwd = PARSER_PRESET;
128-
129140
const actual = await t.throws(cli([], {cwd})('type(scope)-ticket subject'));
141+
130142
t.true(includes(actual.stdout, 'message may not be empty [subject-empty]'));
131143

132-
await cli(['--parser-preset', './parser-preset'], {cwd})('type(scope)-ticket subject');
144+
await cli(['--parser-preset', './parser-preset'], {cwd})(
145+
'type(scope)-ticket subject'
146+
);
133147
});
134148

135149
async function init(cwd) {
@@ -142,5 +156,9 @@ async function init(cwd) {
142156
}
143157

144158
function pkg(cwd) {
145-
return sander.writeFile(cwd, 'package.json', JSON.stringify({scripts: {commitmsg: `${CLI} -e`}}));
159+
return sander.writeFile(
160+
cwd,
161+
'package.json',
162+
JSON.stringify({scripts: {commitmsg: `${CLI} -e`}})
163+
);
146164
}

‎@commitlint/core/fixtures/legacy/.conventional-changelog-lintrc

-5
This file was deleted.

‎@commitlint/core/fixtures/overriden-legacy/.conventional-changelog-lintrc

-5
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
extends: ['./first-extended'],
3+
rules: {
4+
zero: 0
5+
}
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
extends: ['./second-extended'],
3+
rules: {
4+
one: 1
5+
}
6+
};
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module.exports = {
22
rules: {
3-
legacy: false
3+
two: 2
44
}
55
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": ["./first-extended"],
3+
"rules": {
4+
"zero": 0
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
extends: ['./second-extended'],
3+
rules: {
4+
one: 1
5+
}
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
rules: {
3+
two: 2
4+
}
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
extends: ['./second-extended'],
3+
rules: {
4+
one: 1
5+
}
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
rules: {
3+
two: 2
4+
}
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"commitlint": {
3+
"extends": ["./first-extended"],
4+
"rules": {
5+
"zero": 0
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
extends:
2+
- "./first-extended"
3+
rules:
4+
zero: 0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
extends: ['./second-extended'],
3+
rules: {
4+
one: 1
5+
}
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
rules: {
3+
two: 2
4+
}
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
extends: ['./second-extended'],
3+
rules: {
4+
one: 1
5+
}
6+
};
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
module.exports = {
2-
extends: ['./second-extended'],
3-
rules: {
4-
one: 1
5-
}
6-
};
1+
module.exports = require('./commitlint.config.js');
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
const defaultOpts = require('conventional-changelog-angular');
2-
const _ = require('lodash');
3-
4-
module.exports = defaultOpts.then(data => {
5-
const extented = _.cloneDeep(data);
6-
extented.parserOpts.headerPattern = /^(\w*)(?:\((.*)\))?-(.*)$/;
7-
return extented;
1+
module.exports = Promise.resolve().then(() => {
2+
return {
3+
parserOpts: {
4+
headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/
5+
}
6+
};
87
});

‎@commitlint/core/package.json

+6-9
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,15 @@
6363
"license": "MIT",
6464
"devDependencies": {
6565
"@commitlint/utils": "^3.1.1",
66-
"ansi-styles": "3.1.0",
67-
"ava": "0.18.2",
68-
"babel-cli": "^6.18.0",
66+
"ava": "0.22.0",
67+
"babel-cli": "^6.26.0",
6968
"babel-preset-commitlint": "^3.2.0",
70-
"babel-register": "6.24.1",
69+
"babel-register": "^6.26.0",
7170
"cross-env": "^5.0.1",
7271
"denodeify": "1.2.1",
7372
"dependency-check": "2.7.0",
7473
"execa": "0.6.3",
7574
"globby": "6.1.0",
76-
"has-ansi": "3.0.0",
7775
"import-from": "2.1.0",
7876
"nyc": "10.3.2",
7977
"path-exists": "3.0.0",
@@ -82,19 +80,18 @@
8280
"xo": "0.18.2"
8381
},
8482
"dependencies": {
83+
"@marionebl/git-raw-commits": "^1.2.0",
84+
"@marionebl/sander": "^0.6.0",
8585
"babel-runtime": "^6.23.0",
8686
"chalk": "^2.0.1",
8787
"conventional-changelog-angular": "^1.3.3",
8888
"conventional-commits-parser": "^1.3.0",
89+
"cosmiconfig": "^3.0.1",
8990
"find-up": "^2.1.0",
9091
"franc": "^2.0.0",
91-
"git-raw-commits": "^1.1.2",
92-
"import-from": "^2.1.0",
9392
"lodash": "^4.17.4",
94-
"mz": "^2.6.0",
9593
"path-exists": "^3.0.0",
9694
"pos": "^0.4.2",
97-
"rc": "^1.1.7",
9895
"resolve-from": "^3.0.0",
9996
"semver": "^5.3.0"
10097
}

‎@commitlint/core/src/format.test.js

-66
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import test from 'ava';
2-
import hasAnsi from 'has-ansi';
32
import chalk from 'chalk';
4-
import {yellow, red, magenta, blue} from 'ansi-styles';
53
import {includes} from 'lodash';
64
import format from './format';
75

@@ -49,19 +47,6 @@ test('returns a correct of empty .errors and .warnings', t => {
4947
t.true(includes(msg, '1 problems, 1 warnings'));
5048
});
5149

52-
test('colors messages by default', t => {
53-
const [msg] = format({
54-
errors: [],
55-
warnings: []
56-
});
57-
t.true(hasAnsi(msg));
58-
});
59-
60-
test('does not color messages if configured', t => {
61-
const [msg] = format({}, {color: false});
62-
t.false(hasAnsi(msg));
63-
});
64-
6550
test('uses appropriate signs by default', t => {
6651
const [err, warn] = format({
6752
errors: [
@@ -110,54 +95,3 @@ test('uses signs as configured', t => {
11095
t.true(includes(err, 'ERR'));
11196
t.true(includes(warn, 'WRN'));
11297
});
113-
114-
test('uses appropriate colors by default', t => {
115-
const [err, warn] = format({
116-
errors: [
117-
{
118-
level: 2,
119-
name: 'error-name',
120-
message: 'There was an error'
121-
}
122-
],
123-
warnings: [
124-
{
125-
level: 1,
126-
name: 'warning-name',
127-
message: 'There was a problem'
128-
}
129-
]
130-
});
131-
132-
t.true(includes(err, red.open));
133-
t.true(includes(warn, yellow.open));
134-
});
135-
136-
if (process.platform !== 'win32') {
137-
test('uses colors as configured', t => {
138-
const [err, warn] = format(
139-
{
140-
errors: [
141-
{
142-
level: 2,
143-
name: 'error-name',
144-
message: 'There was an error'
145-
}
146-
],
147-
warnings: [
148-
{
149-
level: 1,
150-
name: 'warning-name',
151-
message: 'There was a problem'
152-
}
153-
]
154-
},
155-
{
156-
colors: ['white', 'magenta', 'blue']
157-
}
158-
);
159-
160-
t.true(includes(err, blue.open));
161-
t.true(includes(warn, magenta.open));
162-
});
163-
}

‎@commitlint/core/src/library/parse.test.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import importFrom from 'import-from';
22
import test from 'ava';
33
import parse from './parse';
44

5-
test('throws when called without params', t => {
6-
t.throws(parse(), /Expected a raw commit/);
5+
test('throws when called without params', async t => {
6+
const error = await t.throws(parse());
7+
t.is(error.message, 'Expected a raw commit');
78
});
89

9-
test('throws when called with empty message', t => {
10-
t.throws(parse(''), /Expected a raw commit/);
10+
test('throws when called with empty message', async t => {
11+
const error = await t.throws(parse());
12+
t.is(error.message, 'Expected a raw commit');
1113
});
1214

1315
test('returns object with raw message', async t => {
@@ -16,10 +18,10 @@ test('returns object with raw message', async t => {
1618
t.is(actual.raw, message);
1719
});
1820

19-
test('calls parser with message and passed options', t => {
21+
test('calls parser with message and passed options', async t => {
2022
const message = 'message';
2123

22-
parse(message, m => {
24+
await parse(message, m => {
2325
t.is(message, m);
2426
return {};
2527
});
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import path from 'path';
2+
import up from 'find-up';
3+
4+
export default toplevel;
5+
6+
// Find the next git root
7+
// (start: string) => Promise<string | null>
8+
async function toplevel(cwd) {
9+
const found = await up('.git', {cwd});
10+
11+
if (typeof found !== 'string') {
12+
return found;
13+
}
14+
15+
return path.join(found, '..');
16+
}

‎@commitlint/core/src/lint.test.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import test from 'ava';
22
import lint from './lint';
33

4-
test('throws without params', t => {
5-
t.throws(lint());
4+
test('throws without params', async t => {
5+
const error = await t.throws(lint());
6+
t.is(error.message, 'Expected a raw commit');
67
});
78

8-
test('throws with empty message', t => {
9-
t.throws(lint(''));
9+
test('throws with empty message', async t => {
10+
const error = await t.throws(lint(''));
11+
t.is(error.message, 'Expected a raw commit');
1012
});
1113

1214
test('positive on stub message and no rule', async t => {

‎@commitlint/core/src/load.js

+19-52
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,53 @@
11
import path from 'path';
2-
import importFrom from 'import-from';
2+
import cosmiconfig from 'cosmiconfig';
33
import {entries, merge, mergeWith, pick} from 'lodash';
4-
import rc from 'rc';
54
import resolveFrom from 'resolve-from';
65

7-
import resolveExtends from './library/resolve-extends';
86
import executeRule from './library/execute-rule';
7+
import resolveExtends from './library/resolve-extends';
8+
import toplevel from './library/toplevel';
99

1010
const w = (a, b) => (Array.isArray(b) ? b : undefined);
1111
const valid = input => pick(input, 'extends', 'rules', 'parserPreset');
1212

13-
export default async (seed = {}) => {
14-
// Obtain config from .rc files
15-
const raw = file();
13+
export default async (seed = {}, options = {cwd: ''}) => {
14+
const explorer = cosmiconfig('commitlint', {
15+
rcExtensions: true,
16+
stopDir: await toplevel(options.cwd)
17+
});
18+
19+
const raw = (await explorer.load(options.cwd)) || {};
20+
const base = raw.filepath ? path.dirname(raw.filepath) : options.cwd;
1621

1722
// Merge passed config with file based options
18-
const config = valid(merge(raw, seed));
23+
const config = valid(merge(raw.config, seed));
1924
const opts = merge({extends: [], rules: {}}, pick(config, 'extends'));
2025

2126
// Resolve parserPreset key
2227
if (typeof config.parserPreset === 'string') {
23-
const resolvedParserPreset = resolveFrom(process.cwd(), config.parserPreset);
28+
const resolvedParserPreset = resolveFrom(base, config.parserPreset);
2429

2530
config.parserPreset = {
2631
name: config.parserPreset,
27-
path: `./${path.posix.relative(process.cwd(), resolvedParserPreset)}`.split(path.sep).join('/'),
32+
path: resolvedParserPreset,
2833
opts: require(resolvedParserPreset)
2934
};
3035
}
3136

3237
// Resolve extends key
3338
const extended = resolveExtends(opts, {
3439
prefix: 'commitlint-config',
35-
cwd: raw.config ? path.dirname(raw.config) : process.cwd(),
40+
cwd: base,
3641
parserPreset: config.parserPreset
3742
});
3843

3944
const preset = valid(mergeWith(extended, config, w));
4045

4146
// Await parser-preset if applicable
42-
if (typeof preset.parserPreset === 'object' && typeof preset.parserPreset.opts === 'object') {
47+
if (
48+
typeof preset.parserPreset === 'object' &&
49+
typeof preset.parserPreset.opts === 'object'
50+
) {
4351
preset.parserPreset.opts = await preset.parserPreset.opts;
4452
}
4553

@@ -72,44 +80,3 @@ export default async (seed = {}) => {
7280
return registry;
7381
}, preset);
7482
};
75-
76-
function file() {
77-
const legacy = rc('conventional-changelog-lint');
78-
const legacyFound = typeof legacy.config === 'string';
79-
80-
const found = resolveable('./commitlint.config');
81-
const raw = found ? importFrom(process.cwd(), './commitlint.config') : {};
82-
83-
if (legacyFound && !found) {
84-
console.warn(
85-
`Using legacy ${path.relative(
86-
process.cwd(),
87-
legacy.config
88-
)}. Rename to commitlint.config.js to silence this warning.`
89-
);
90-
}
91-
92-
if (legacyFound && found) {
93-
console.warn(
94-
`Ignored legacy ${path.relative(
95-
process.cwd(),
96-
legacy.config
97-
)} as commitlint.config.js superseeds it. Remove .conventional-changelog-lintrc to silence this warning.`
98-
);
99-
}
100-
101-
if (found) {
102-
return raw;
103-
}
104-
105-
return legacy;
106-
}
107-
108-
function resolveable(id) {
109-
try {
110-
resolveFrom(process.cwd(), id);
111-
return true;
112-
} catch (err) {
113-
return false;
114-
}
115-
}

‎@commitlint/core/src/load.test.js

+103-59
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
1-
import path from 'path';
21
import test from 'ava';
32

3+
import {bootstrap} from './test-git';
44
import load from './load';
55

6-
const cwd = process.cwd();
7-
8-
test.afterEach.always(t => {
9-
t.context.back();
10-
});
11-
126
test('extends-empty should have no rules', async t => {
13-
t.context.back = chdir('fixtures/extends-empty');
14-
const actual = await load();
7+
const cwd = await bootstrap('fixtures/extends-empty');
8+
const actual = await load({}, {cwd});
159
t.deepEqual(actual.rules, {});
1610
});
1711

1812
test('uses seed as configured', async t => {
19-
t.context.back = chdir('fixtures/extends-empty');
20-
const actual = await load({rules: {foo: 'bar'}});
13+
const cwd = await bootstrap('fixtures/extends-empty');
14+
const actual = await load({rules: {foo: 'bar'}}, {cwd});
2115
t.is(actual.rules.foo, 'bar');
2216
});
2317

2418
test('uses seed with parserPreset', async t => {
25-
t.context.back = chdir('fixtures/parser-preset');
19+
const cwd = await bootstrap('fixtures/parser-preset');
20+
const {parserPreset: actual} = await load(
21+
{
22+
parserPreset: './conventional-changelog-custom'
23+
},
24+
{cwd}
25+
);
2626

27-
const {parserPreset: actual} = await load({parserPreset: './conventional-changelog-custom'});
2827
t.is(actual.name, './conventional-changelog-custom');
2928
t.deepEqual(actual.opts, {
3029
parserOpts: {
@@ -33,26 +32,94 @@ test('uses seed with parserPreset', async t => {
3332
});
3433
});
3534

36-
test('invalid extend should throw', t => {
37-
t.context.back = chdir('fixtures/extends-invalid');
38-
t.throws(load());
35+
test('invalid extend should throw', async t => {
36+
const cwd = await bootstrap('fixtures/extends-invalid');
37+
await t.throws(load({}, {cwd}));
3938
});
4039

4140
test('empty file should have no rules', async t => {
42-
t.context.back = chdir('fixtures/empty-object-file');
43-
const actual = await load();
41+
const cwd = await bootstrap('fixtures/empty-object-file');
42+
const actual = await load({}, {cwd});
4443
t.deepEqual(actual.rules, {});
4544
});
4645

4746
test('empty file should extend nothing', async t => {
48-
t.context.back = chdir('fixtures/empty-file');
49-
const actual = await load();
47+
const cwd = await bootstrap('fixtures/empty-file');
48+
const actual = await load({}, {cwd});
5049
t.deepEqual(actual.extends, []);
5150
});
5251

52+
test('respects cwd option', async t => {
53+
const cwd = await bootstrap('fixtures/recursive-extends/first-extended');
54+
const actual = await load({}, {cwd});
55+
t.deepEqual(actual, {
56+
extends: ['./second-extended'],
57+
rules: {
58+
one: 1,
59+
two: 2
60+
}
61+
});
62+
});
63+
5364
test('recursive extends', async t => {
54-
t.context.back = chdir('fixtures/recursive-extends');
55-
const actual = await load();
65+
const cwd = await bootstrap('fixtures/recursive-extends');
66+
const actual = await load({}, {cwd});
67+
t.deepEqual(actual, {
68+
extends: ['./first-extended'],
69+
rules: {
70+
zero: 0,
71+
one: 1,
72+
two: 2
73+
}
74+
});
75+
});
76+
77+
test('recursive extends with json file', async t => {
78+
const cwd = await bootstrap('fixtures/recursive-extends-json');
79+
const actual = await load({}, {cwd});
80+
81+
t.deepEqual(actual, {
82+
extends: ['./first-extended'],
83+
rules: {
84+
zero: 0,
85+
one: 1,
86+
two: 2
87+
}
88+
});
89+
});
90+
91+
test('recursive extends with yaml file', async t => {
92+
const cwd = await bootstrap('fixtures/recursive-extends-yaml');
93+
const actual = await load({}, {cwd});
94+
95+
t.deepEqual(actual, {
96+
extends: ['./first-extended'],
97+
rules: {
98+
zero: 0,
99+
one: 1,
100+
two: 2
101+
}
102+
});
103+
});
104+
105+
test('recursive extends with js file', async t => {
106+
const cwd = await bootstrap('fixtures/recursive-extends-js');
107+
const actual = await load({}, {cwd});
108+
109+
t.deepEqual(actual, {
110+
extends: ['./first-extended'],
111+
rules: {
112+
zero: 0,
113+
one: 1,
114+
two: 2
115+
}
116+
});
117+
});
118+
119+
test('recursive extends with package.json file', async t => {
120+
const cwd = await bootstrap('fixtures/recursive-extends-package');
121+
const actual = await load({}, {cwd});
122+
56123
t.deepEqual(actual, {
57124
extends: ['./first-extended'],
58125
rules: {
@@ -64,8 +131,8 @@ test('recursive extends', async t => {
64131
});
65132

66133
test('parser preset overwrites completely instead of merging', async t => {
67-
t.context.back = chdir('fixtures/parser-preset-override');
68-
const actual = await load();
134+
const cwd = await bootstrap('fixtures/parser-preset-override');
135+
const actual = await load({}, {cwd});
69136

70137
t.is(actual.parserPreset.name, './custom');
71138
t.is(typeof actual.parserPreset.opts, 'object');
@@ -78,17 +145,21 @@ test('parser preset overwrites completely instead of merging', async t => {
78145
});
79146

80147
test('recursive extends with parserPreset', async t => {
81-
t.context.back = chdir('fixtures/recursive-parser-preset');
82-
const actual = await load();
148+
const cwd = await bootstrap('fixtures/recursive-parser-preset');
149+
const actual = await load({}, {cwd});
83150

84151
t.is(actual.parserPreset.name, './conventional-changelog-custom');
85152
t.is(typeof actual.parserPreset.opts, 'object');
86-
t.deepEqual(actual.parserPreset.opts.parserOpts.headerPattern, /^(\w*)(?:\((.*)\))?-(.*)$/);
153+
t.deepEqual(
154+
actual.parserPreset.opts.parserOpts.headerPattern,
155+
/^(\w*)(?:\((.*)\))?-(.*)$/
156+
);
87157
});
88158

89159
test('ignores unknow keys', async t => {
90-
t.context.back = chdir('fixtures/trash-file');
91-
const actual = await load();
160+
const cwd = await bootstrap('fixtures/trash-file');
161+
const actual = await load({}, {cwd});
162+
92163
t.deepEqual(actual, {
93164
extends: [],
94165
rules: {
@@ -99,8 +170,9 @@ test('ignores unknow keys', async t => {
99170
});
100171

101172
test('ignores unknow keys recursively', async t => {
102-
t.context.back = chdir('fixtures/trash-extend');
103-
const actual = await load();
173+
const cwd = await bootstrap('fixtures/trash-extend');
174+
const actual = await load({}, {cwd});
175+
104176
t.deepEqual(actual, {
105177
extends: ['./one'],
106178
rules: {
@@ -109,31 +181,3 @@ test('ignores unknow keys recursively', async t => {
109181
}
110182
});
111183
});
112-
113-
test('supports legacy .conventional-changelog-lintrc', async t => {
114-
t.context.back = chdir('fixtures/legacy');
115-
const actual = await load();
116-
t.deepEqual(actual, {
117-
extends: [],
118-
rules: {
119-
legacy: true
120-
}
121-
});
122-
});
123-
124-
test('commitlint.config.js overrides .conventional-changelog-lintrc', async t => {
125-
t.context.back = chdir('fixtures/overriden-legacy');
126-
const actual = await load();
127-
t.deepEqual(actual, {
128-
extends: [],
129-
rules: {
130-
legacy: false
131-
}
132-
});
133-
});
134-
135-
function chdir(target) {
136-
const to = path.resolve(cwd, target.split('/').join(path.sep));
137-
process.chdir(to);
138-
return () => process.chdir(cwd);
139-
}

‎@commitlint/core/src/read.js

+19-30
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import path from 'path';
22
import exists from 'path-exists';
3-
import up from 'find-up';
4-
import gitRawCommits from 'git-raw-commits';
5-
import {readFile} from 'mz/fs';
3+
import gitRawCommits from '@marionebl/git-raw-commits';
4+
import * as sander from '@marionebl/sander';
5+
6+
import toplevel from './library/toplevel';
67

78
export default getCommitMessages;
89

@@ -15,25 +16,25 @@ const SHALLOW_MESSAGE = [
1516
// Get commit messages
1617
// Object => Promise<Array<String>>
1718
async function getCommitMessages(settings) {
18-
const {from, to, edit} = settings;
19+
const {cwd, from, to, edit} = settings;
1920

2021
if (edit) {
21-
return getEditCommit();
22+
return getEditCommit(cwd);
2223
}
2324

24-
if (await isShallow()) {
25+
if (await isShallow(cwd)) {
2526
throw new Error(SHALLOW_MESSAGE);
2627
}
2728

28-
return getHistoryCommits({from, to});
29+
return getHistoryCommits({from, to}, {cwd});
2930
}
3031

3132
// Get commit messages from history
3233
// Object => Promise<string[]>
33-
function getHistoryCommits(options) {
34+
function getHistoryCommits(options, opts = {}) {
3435
return new Promise((resolve, reject) => {
3536
const data = [];
36-
gitRawCommits(options)
37+
gitRawCommits(options, {cwd: opts.cwd})
3738
.on('data', chunk => data.push(chunk.toString('utf-8')))
3839
.on('error', reject)
3940
.on('end', () => {
@@ -43,40 +44,28 @@ function getHistoryCommits(options) {
4344
}
4445

4546
// Check if the current repository is shallow
46-
// () => Promise<Boolean>
47-
async function isShallow() {
48-
const top = await toplevel();
47+
// (cwd: string) => Promise<Boolean>
48+
async function isShallow(cwd) {
49+
const top = await toplevel(cwd);
4950

5051
if (typeof top !== 'string') {
51-
throw new TypeError(`Could not find git root - is this a git repository?`);
52+
throw new TypeError(`Could not find git root from ${cwd}`);
5253
}
5354

5455
const shallow = path.join(top, '.git/shallow');
5556
return exists(shallow);
5657
}
5758

5859
// Get recently edited commit message
59-
// () => Promise<Array<String>>
60-
async function getEditCommit() {
61-
const top = await toplevel();
60+
// (cwd: string) => Promise<Array<String>>
61+
async function getEditCommit(cwd) {
62+
const top = await toplevel(cwd);
6263

6364
if (typeof top !== 'string') {
64-
throw new TypeError(`Could not find git root - is this a git repository?`);
65+
throw new TypeError(`Could not find git root from ${cwd}`);
6566
}
6667

6768
const editFilePath = path.join(top, '.git/COMMIT_EDITMSG');
68-
const editFile = await readFile(editFilePath);
69+
const editFile = await sander.readFile(editFilePath);
6970
return [`${editFile.toString('utf-8')}\n`];
7071
}
71-
72-
// Find the next git root
73-
// (start: string) => Promise<string | null>
74-
async function toplevel(cwd = process.cwd()) {
75-
const found = await up('.git', {cwd});
76-
77-
if (typeof found !== 'string') {
78-
return found;
79-
}
80-
81-
return path.join(found, '..');
82-
}

‎@commitlint/core/src/read.test.js

+27-90
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,53 @@
1-
import {tmpdir} from 'os';
2-
import crypto from 'crypto';
3-
import {join} from 'path';
4-
51
import test from 'ava';
6-
import denodeify from 'denodeify';
72
import execa from 'execa';
8-
import {mkdir, writeFile} from 'mz/fs';
9-
import exists from 'path-exists';
10-
import rimraf from 'rimraf';
3+
import * as sander from '@marionebl/sander';
114

125
import pkg from '../package';
6+
import {bootstrap, clone} from './test-git';
137
import read from './read';
148

15-
const rm = denodeify(rimraf);
16-
17-
test.beforeEach(async t => {
18-
t.context.repos = [await initRepository()];
19-
});
20-
21-
test.afterEach.always(async t => {
22-
try {
23-
await Promise.all(t.context.repos.map(async repo => cleanRepository(repo)));
24-
t.context.repos = [];
25-
} catch (err) {
26-
console.log({err});
27-
}
28-
});
9+
test('get edit commit message from git root', async t => {
10+
const cwd = await bootstrap();
2911

30-
test.serial('get edit commit message from git root', async t => {
31-
await writeFile('alpha.txt', 'alpha');
32-
await execa('git', ['add', '.']);
33-
await execa('git', ['commit', '-m', 'alpha']);
12+
await sander.writeFile(cwd, 'alpha.txt', 'alpha');
13+
await execa('git', ['add', '.'], {cwd});
14+
await execa('git', ['commit', '-m', 'alpha'], {cwd});
3415
const expected = ['alpha\n\n'];
35-
const actual = await read({edit: true});
16+
const actual = await read({edit: true, cwd});
3617
t.deepEqual(actual, expected);
3718
});
3819

39-
test.serial('get history commit messages', async t => {
40-
await writeFile('alpha.txt', 'alpha');
41-
await execa('git', ['add', 'alpha.txt']);
42-
await execa('git', ['commit', '-m', 'alpha']);
43-
await execa('git', ['rm', 'alpha.txt']);
44-
await execa('git', ['commit', '-m', 'remove alpha']);
20+
test('get history commit messages', async t => {
21+
const cwd = await bootstrap();
22+
await sander.writeFile(cwd, 'alpha.txt', 'alpha');
23+
await execa('git', ['add', 'alpha.txt'], {cwd});
24+
await execa('git', ['commit', '-m', 'alpha'], {cwd});
25+
await execa('git', ['rm', 'alpha.txt'], {cwd});
26+
await execa('git', ['commit', '-m', 'remove alpha'], {cwd});
4527

4628
const expected = ['remove alpha\n\n', 'alpha\n\n'];
47-
const actual = await read({});
29+
const actual = await read({cwd});
4830
t.deepEqual(actual, expected);
4931
});
5032

51-
test.serial('get edit commit message from git subdirectory', async t => {
52-
await mkdir('beta');
53-
await writeFile('beta/beta.txt', 'beta');
54-
process.chdir('beta');
55-
await execa('git', ['add', '.']);
56-
await execa('git', ['commit', '-m', 'beta']);
33+
test('get edit commit message from git subdirectory', async t => {
34+
const cwd = await bootstrap();
35+
await sander.mkdir(cwd, 'beta');
36+
await sander.writeFile(cwd, 'beta/beta.txt', 'beta');
37+
38+
await execa('git', ['add', '.'], {cwd});
39+
await execa('git', ['commit', '-m', 'beta'], {cwd});
5740

5841
const expected = ['beta\n\n'];
59-
const actual = await read({edit: true});
42+
const actual = await read({edit: true, cwd});
6043
t.deepEqual(actual, expected);
6144
});
6245

63-
test.serial('get history commit messages from shallow clone', async t => {
64-
const [repo] = t.context.repos;
65-
66-
await writeFile('alpha.txt', 'alpha');
67-
await execa('git', ['add', 'alpha.txt']);
68-
await execa('git', ['commit', '-m', 'alpha']);
69-
70-
const clone = await cloneRepository(pkg.repository.url, repo, '--depth', '1');
71-
t.context.repos = [...t.context.repos, clone];
46+
test('get history commit messages from shallow clone', async t => {
47+
const cwd = await clone(pkg.repository.url, '--depth', '1');
48+
const err = await t.throws(read({from: 'master', cwd}));
7249

73-
const err = await t.throws(read({from: 'master'}));
7450
t.true(
7551
err.message.indexOf('Could not get git history from shallow clone') > -1
7652
);
7753
});
78-
79-
async function initRepository() {
80-
const previous = process.cwd();
81-
const directory = join(tmpdir(), rand());
82-
83-
await execa('git', ['init', directory]);
84-
85-
process.chdir(directory);
86-
87-
await execa('git', ['config', 'user.email', 'test@example.com']);
88-
await execa('git', ['config', 'user.name', 'ava']);
89-
90-
return {directory, previous};
91-
}
92-
93-
async function cloneRepository(source, context, ...args) {
94-
const directory = join(tmpdir(), rand());
95-
await execa('git', ['clone', ...args, source, directory]);
96-
process.chdir(directory);
97-
98-
await execa('git', ['config', 'user.email', 'test@example.com']);
99-
await execa('git', ['config', 'user.name', 'ava']);
100-
101-
return {directory, previous: context.previous};
102-
}
103-
104-
async function cleanRepository(repo) {
105-
if (repo.previous && repo.previous !== process.cwd()) {
106-
process.chdir(repo.previous);
107-
}
108-
109-
if (await exists(repo.directory)) {
110-
await rm(repo.directory);
111-
}
112-
}
113-
114-
function rand() {
115-
return crypto.randomBytes(Math.ceil(6)).toString('hex').slice(0, 12);
116-
}

‎@commitlint/core/src/test-git.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import crypto from 'crypto';
2+
import os from 'os';
3+
import path from 'path';
4+
5+
import execa from 'execa';
6+
import * as sander from '@marionebl/sander';
7+
8+
export {bootstrap, clone};
9+
10+
const PKG_ROOT = path.join(__dirname, '..');
11+
12+
async function bootstrap(fixture) {
13+
const cwd = path.join(os.tmpdir(), rand());
14+
15+
if (typeof fixture !== 'undefined') {
16+
await sander.copydir(PKG_ROOT, fixture).to(cwd);
17+
}
18+
19+
await execa('git', ['init', cwd]);
20+
await setup(cwd);
21+
return cwd;
22+
}
23+
24+
async function clone(source, ...args) {
25+
const cwd = path.join(os.tmpdir(), rand());
26+
await execa('git', ['clone', ...args, source, cwd]);
27+
await setup(cwd);
28+
return cwd;
29+
}
30+
31+
async function setup(cwd) {
32+
await execa('git', ['config', 'user.name', 'ava'], {cwd});
33+
await execa('git', ['config', 'user.email', 'test@example.com'], {cwd});
34+
}
35+
36+
function rand() {
37+
return crypto
38+
.randomBytes(Math.ceil(6))
39+
.toString('hex')
40+
.slice(0, 12);
41+
}

‎README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ echo "module.exports = {extends: ['@commitlint/config-angular']}" > commitlint.c
2828

2929
## Config
3030

31-
* Configuration is picked up from `commitlint.config.js` files
31+
* Configuration is picked up from `commitlint.config.js`, `.commitlintrc.js`, `.commitlintrc.json`, or `.commitlintrc.yml` file or a `commitlint` field in `package.json`
3232
* Packages: [cli](./@commitlint/cli), [core](./@commitlint/core)
3333
* See [Rules](./docs/reference-rules.md) for a complete list of possible rules
3434
* An example configuration can be found at [@commitlint/config-angular](./@commitlint/config-angular/index.js)

‎docs/guides-ci-setup.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ npm install --save-dev @commitlint/{config-angular,cli}
1818
echo "module.exports = {extends: ['@commitlint/config-angular']};" > commitlint.config.js
1919
```
2020

21+
Alternatively the configuration can be defined in `.commitlintrc.js`, `.commitlintrc.json`, or `.commitlintrc.yml` file or a `commitlint` field in `package.json`.
22+
2123
## First test run with Travis
2224

2325
Add a `.travis.yml` to your project root

‎docs/guides-local-setup.md

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ npm install --save-dev @commitlint/{cli,config-angular}
1818
echo "module.exports = {extends: ['@commitlint/config-angular']};" > commitlint.config.js
1919
```
2020

21+
Alternatively the configuration can be defined in `.commitlintrc.js`, `.commitlintrc.json`, or `.commitlintrc.yml` file or a `commitlint` field in `package.json`.
22+
2123
## Install husky
2224

2325
Install `husky` as devDependency, a handy git hook helper available on npm.

‎package.json

+3
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,8 @@
6565
"prettier": "^1.5.2",
6666
"trevor": "^2.3.0",
6767
"xo": "^0.18.2"
68+
},
69+
"dependencies": {
70+
"@marionebl/sander": "^0.6.1"
6871
}
6972
}

0 commit comments

Comments
 (0)
Please sign in to comment.