1 | #!/usr/bin/env node
|
2 | require('babel-polyfill');
|
3 |
|
4 | const load = require('@commitlint/load');
|
5 | const lint = require('@commitlint/lint');
|
6 | const read = require('@commitlint/read');
|
7 | const meow = require('meow');
|
8 | const {merge, pick} = require('lodash');
|
9 | const stdin = require('get-stdin');
|
10 | const resolveFrom = require('resolve-from');
|
11 | const resolveGlobal = require('resolve-global');
|
12 |
|
13 | const pkg = require('../package');
|
14 | const help = require('./help');
|
15 |
|
16 | const flags = {
|
17 | color: {
|
18 | alias: 'c',
|
19 | default: true,
|
20 | description: 'toggle colored output',
|
21 | type: 'boolean'
|
22 | },
|
23 | config: {
|
24 | alias: 'g',
|
25 | default: null,
|
26 | description: 'path to the config file',
|
27 | type: 'string'
|
28 | },
|
29 | cwd: {
|
30 | alias: 'd',
|
31 | default: process.cwd(),
|
32 | description: 'directory to execute in',
|
33 | type: 'string'
|
34 | },
|
35 | edit: {
|
36 | alias: 'e',
|
37 | default: false,
|
38 | description:
|
39 | 'read last commit message from the specified file or fallbacks to ./.git/COMMIT_EDITMSG',
|
40 | type: 'string'
|
41 | },
|
42 | env: {
|
43 | alias: 'E',
|
44 | default: null,
|
45 | description:
|
46 | 'check message in the file at path given by environment variable value',
|
47 | type: 'string'
|
48 | },
|
49 | extends: {
|
50 | alias: 'x',
|
51 | description: 'array of shareable configurations to extend',
|
52 | type: 'string'
|
53 | },
|
54 | help: {
|
55 | alias: 'h',
|
56 | type: 'boolean',
|
57 | description: 'display this help message'
|
58 | },
|
59 | from: {
|
60 | alias: 'f',
|
61 | default: null,
|
62 | description: 'lower end of the commit range to lint; applies if edit=false',
|
63 | type: 'string'
|
64 | },
|
65 | format: {
|
66 | alias: 'o',
|
67 | default: null,
|
68 | description: 'output format of the results',
|
69 | type: 'string'
|
70 | },
|
71 | 'parser-preset': {
|
72 | alias: 'p',
|
73 | description: 'configuration preset to use for conventional-commits-parser',
|
74 | type: 'string'
|
75 | },
|
76 | quiet: {
|
77 | alias: 'q',
|
78 | default: false,
|
79 | description: 'toggle console output',
|
80 | type: 'boolean'
|
81 | },
|
82 | to: {
|
83 | alias: 't',
|
84 | default: null,
|
85 | description: 'upper end of the commit range to lint; applies if edit=false',
|
86 | type: 'string'
|
87 | },
|
88 | version: {
|
89 | alias: 'v',
|
90 | type: 'boolean',
|
91 | description: 'display version information'
|
92 | }
|
93 | };
|
94 |
|
95 | const cli = meow({
|
96 | description: `${pkg.name}@${pkg.version} - ${pkg.description}`,
|
97 | flags,
|
98 | help: `[input] reads from stdin if --edit, --env, --from and --to are omitted\n${help(
|
99 | flags
|
100 | )}`,
|
101 | unknown(arg) {
|
102 | throw new Error(`unknown flags: ${arg}`);
|
103 | }
|
104 | });
|
105 |
|
106 | main(cli).catch(err =>
|
107 | setTimeout(() => {
|
108 | if (err.type === pkg.name) {
|
109 | process.exit(1);
|
110 | }
|
111 | throw err;
|
112 | })
|
113 | );
|
114 |
|
115 | async function main(options) {
|
116 | const raw = options.input;
|
117 | const flags = normalizeFlags(options.flags);
|
118 | const fromStdin = checkFromStdin(raw, flags);
|
119 |
|
120 | const range = pick(flags, 'edit', 'from', 'to');
|
121 |
|
122 | const input = await (fromStdin ? stdin() : read(range, {cwd: flags.cwd}));
|
123 |
|
124 | const messages = (Array.isArray(input) ? input : [input])
|
125 | .filter(message => typeof message === 'string')
|
126 | .filter(Boolean);
|
127 |
|
128 | if (messages.length === 0 && !checkFromRepository(flags)) {
|
129 | const err = new Error(
|
130 | '[input] is required: supply via stdin, or --env or --edit or --from and --to'
|
131 | );
|
132 | err.type = pkg.name;
|
133 | console.log(`${cli.help}\n`);
|
134 | console.log(err.message);
|
135 | throw err;
|
136 | }
|
137 |
|
138 | const loadOpts = {cwd: flags.cwd, file: flags.config};
|
139 | const loaded = await load(getSeed(flags), loadOpts);
|
140 | const parserOpts = selectParserOpts(loaded.parserPreset);
|
141 | const opts = parserOpts ? {parserOpts} : {parserOpts: {}};
|
142 | const format = loadFormatter(loaded, flags);
|
143 |
|
144 |
|
145 | if (range.edit) {
|
146 | opts.parserOpts.commentChar = '#';
|
147 | }
|
148 |
|
149 | const results = await Promise.all(
|
150 | messages.map(message => lint(message, loaded.rules, opts))
|
151 | );
|
152 |
|
153 | if (Object.keys(loaded.rules).length === 0) {
|
154 | results.push({
|
155 | valid: false,
|
156 | errors: [
|
157 | {
|
158 | level: 2,
|
159 | valid: false,
|
160 | name: 'empty-rules',
|
161 | message: [
|
162 | 'Please add rules to your `commitlint.config.js`',
|
163 | ' - Getting started guide: https://git.io/fpUzJ',
|
164 | ' - Example config: https://git.io/fpUzm'
|
165 | ].join('\n')
|
166 | }
|
167 | ],
|
168 | warnings: [],
|
169 | input: ''
|
170 | });
|
171 | }
|
172 |
|
173 | const report = results.reduce(
|
174 | (info, result) => {
|
175 | info.valid = result.valid ? info.valid : false;
|
176 | info.errorCount += result.errors.length;
|
177 | info.warningCount += result.warnings.length;
|
178 | info.results.push(result);
|
179 |
|
180 | return info;
|
181 | },
|
182 | {
|
183 | valid: true,
|
184 | errorCount: 0,
|
185 | warningCount: 0,
|
186 | results: []
|
187 | }
|
188 | );
|
189 |
|
190 | const output = format(report, {color: flags.color});
|
191 |
|
192 | if (!flags.quiet) {
|
193 | console.log(output);
|
194 | }
|
195 |
|
196 | if (!report.valid) {
|
197 | const err = new Error(output);
|
198 | err.type = pkg.name;
|
199 | throw err;
|
200 | }
|
201 | }
|
202 |
|
203 | function checkFromStdin(input, flags) {
|
204 | return input.length === 0 && !checkFromRepository(flags);
|
205 | }
|
206 |
|
207 | function checkFromRepository(flags) {
|
208 | return checkFromHistory(flags) || checkFromEdit(flags);
|
209 | }
|
210 |
|
211 | function checkFromEdit(flags) {
|
212 | return Boolean(flags.edit) || flags.env;
|
213 | }
|
214 |
|
215 | function checkFromHistory(flags) {
|
216 | return typeof flags.from === 'string' || typeof flags.to === 'string';
|
217 | }
|
218 |
|
219 | function normalizeFlags(flags) {
|
220 | const edit = getEditValue(flags);
|
221 | return merge({}, flags, {edit, e: edit});
|
222 | }
|
223 |
|
224 | function getEditValue(flags) {
|
225 | if (flags.env) {
|
226 | if (!(flags.env in process.env)) {
|
227 | throw new Error(
|
228 | `Recieved '${
|
229 | flags.env
|
230 | }' as value for -E | --env, but environment variable '${
|
231 | flags.env
|
232 | }' is not available globally`
|
233 | );
|
234 | }
|
235 | return process.env[flags.env];
|
236 | }
|
237 | const {edit} = flags;
|
238 |
|
239 |
|
240 | if (edit === '') {
|
241 | return true;
|
242 | }
|
243 | if (typeof edit === 'boolean') {
|
244 | return edit;
|
245 | }
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | const isGitParams = edit === '$GIT_PARAMS' || edit === '%GIT_PARAMS%';
|
252 | const isHuskyParams =
|
253 | edit === '$HUSKY_GIT_PARAMS' || edit === '%HUSKY_GIT_PARAMS%';
|
254 |
|
255 | if (isGitParams || isHuskyParams) {
|
256 | console.warn(`Using environment variable syntax (${edit}) in -e |\
|
257 | --edit is deprecated. Use '{-E|--env} HUSKY_GIT_PARAMS instead'`);
|
258 |
|
259 | if (isGitParams && 'GIT_PARAMS' in process.env) {
|
260 | return process.env.GIT_PARAMS;
|
261 | }
|
262 | if ('HUSKY_GIT_PARAMS' in process.env) {
|
263 | return process.env.HUSKY_GIT_PARAMS;
|
264 | }
|
265 | throw new Error(
|
266 | `Received ${edit} as value for -e | --edit, but GIT_PARAMS or HUSKY_GIT_PARAMS are not available globally.`
|
267 | );
|
268 | }
|
269 | return edit;
|
270 | }
|
271 |
|
272 | function getSeed(seed) {
|
273 | const e = Array.isArray(seed.extends) ? seed.extends : [seed.extends];
|
274 | const n = e.filter(i => typeof i === 'string');
|
275 | return n.length > 0
|
276 | ? {extends: n, parserPreset: seed.parserPreset}
|
277 | : {parserPreset: seed.parserPreset};
|
278 | }
|
279 |
|
280 | function selectParserOpts(parserPreset) {
|
281 | if (typeof parserPreset !== 'object') {
|
282 | return undefined;
|
283 | }
|
284 |
|
285 | if (typeof parserPreset.parserOpts !== 'object') {
|
286 | return undefined;
|
287 | }
|
288 |
|
289 | return parserPreset.parserOpts;
|
290 | }
|
291 |
|
292 | function loadFormatter(config, flags) {
|
293 | const moduleName = flags.format || config.formatter || '@commitlint/format';
|
294 | const modulePath =
|
295 | resolveFrom.silent(__dirname, moduleName) ||
|
296 | resolveFrom.silent(flags.cwd, moduleName) ||
|
297 | resolveGlobal.silent(moduleName);
|
298 |
|
299 | if (modulePath) {
|
300 | return require(modulePath);
|
301 | }
|
302 |
|
303 | throw new Error(`Using format ${moduleName}, but cannot find the module.`);
|
304 | }
|
305 |
|
306 |
|
307 | process.on('unhandledRejection', (reason, promise) => {
|
308 | console.log('Unhandled Rejection at: Promise ', promise, ' reason: ', reason);
|
309 | throw reason;
|
310 | });
|