diff --git a/@commitlint/cli/src/cli.test.ts b/@commitlint/cli/src/cli.test.ts index 6e431a065b..90e5e67c50 100644 --- a/@commitlint/cli/src/cli.test.ts +++ b/@commitlint/cli/src/cli.test.ts @@ -548,6 +548,7 @@ test('should print help', async () => { -x, --extends array of shareable configurations to extend [array] -H, --help-url help url in error message [string] -f, --from lower end of the commit range to lint; applies if edit=false [string] + --from-last-tag uses the last tag as the lower end of the commit range to lint; applies if edit=false and from is not set [boolean] --git-log-args additional git log arguments as space separated string, example '--first-parent --cherry-pick' [string] -l, --last just analyze the last commit; applies if edit=false [boolean] -o, --format output format of the results [string] diff --git a/@commitlint/cli/src/cli.ts b/@commitlint/cli/src/cli.ts index ead3631566..5375fa6f2e 100644 --- a/@commitlint/cli/src/cli.ts +++ b/@commitlint/cli/src/cli.ts @@ -90,6 +90,11 @@ const cli = yargs(process.argv.slice(2)) 'lower end of the commit range to lint; applies if edit=false', type: 'string', }, + 'from-last-tag': { + description: + 'uses the last tag as the lower end of the commit range to lint; applies if edit=false and from is not set', + type: 'boolean', + }, 'git-log-args': { description: "additional git log arguments as space separated string, example '--first-parent --cherry-pick'", @@ -242,6 +247,7 @@ async function main(args: MainArgs): Promise { : read({ to: flags.to, from: flags.from, + fromLastTag: flags['from-last-tag'], last: flags.last, edit: flags.edit, cwd: flags.cwd, @@ -400,6 +406,7 @@ function checkFromEdit(flags: CliFlags): boolean { function checkFromHistory(flags: CliFlags): boolean { return ( typeof flags.from === 'string' || + typeof flags['from-last-tag'] === 'boolean' || typeof flags.to === 'string' || typeof flags.last === 'boolean' ); diff --git a/@commitlint/cli/src/types.ts b/@commitlint/cli/src/types.ts index 42056cc556..c9b3a1b8ef 100644 --- a/@commitlint/cli/src/types.ts +++ b/@commitlint/cli/src/types.ts @@ -8,6 +8,7 @@ export interface CliFlags { help?: boolean; 'help-url'?: string; from?: string; + 'from-last-tag'?: boolean; 'git-log-args'?: string; last?: boolean; format?: string; diff --git a/@commitlint/read/src/read.test.ts b/@commitlint/read/src/read.test.ts index f9c38ee70c..dc15042916 100644 --- a/@commitlint/read/src/read.test.ts +++ b/@commitlint/read/src/read.test.ts @@ -72,3 +72,61 @@ test('get edit commit message while skipping first commit', async () => { const actual = await read({from: 'HEAD~2', cwd, gitLogArgs: '--skip 1'}); expect(actual).toEqual(expected); }); + +test('should only read the last commit', async () => { + const cwd: string = await git.bootstrap(); + + await execa('git', ['commit', '--allow-empty', '-m', 'commit Z'], {cwd}); + await execa('git', ['commit', '--allow-empty', '-m', 'commit Y'], {cwd}); + await execa('git', ['commit', '--allow-empty', '-m', 'commit X'], {cwd}); + + const result = await read({cwd, last: true}); + + expect(result).toEqual(['commit X']); +}); + +test('should read commits from the last annotated tag', async () => { + const cwd: string = await git.bootstrap(); + + await execa( + 'git', + ['commit', '--allow-empty', '-m', 'chore: release v1.0.0'], + {cwd} + ); + await execa('git', ['tag', 'v1.0.0', '--annotate', '-m', 'v1.0.0'], {cwd}); + await execa('git', ['commit', '--allow-empty', '-m', 'commit 1'], {cwd}); + await execa('git', ['commit', '--allow-empty', '-m', 'commit 2'], {cwd}); + + const result = await read({cwd, fromLastTag: true}); + + expect(result).toEqual(['commit 2\n\n', 'commit 1\n\n']); +}); + +test('should read commits from the last lightweight tag', async () => { + const cwd: string = await git.bootstrap(); + + await execa( + 'git', + ['commit', '--allow-empty', '-m', 'chore: release v9.9.9-alpha.1'], + {cwd} + ); + await execa('git', ['tag', 'v9.9.9-alpha.1'], {cwd}); + await execa('git', ['commit', '--allow-empty', '-m', 'commit A'], {cwd}); + await execa('git', ['commit', '--allow-empty', '-m', 'commit B'], {cwd}); + + const result = await read({cwd, fromLastTag: true}); + + expect(result).toEqual(['commit B\n\n', 'commit A\n\n']); +}); + +test('should not read any commits when there are no tags', async () => { + const cwd: string = await git.bootstrap(); + + await execa('git', ['commit', '--allow-empty', '-m', 'commit 7'], {cwd}); + await execa('git', ['commit', '--allow-empty', '-m', 'commit 8'], {cwd}); + await execa('git', ['commit', '--allow-empty', '-m', 'commit 9'], {cwd}); + + const result = await read({cwd, fromLastTag: true}); + + expect(result).toHaveLength(0); +}); diff --git a/@commitlint/read/src/read.ts b/@commitlint/read/src/read.ts index 0972c8b0d9..4d483aa474 100644 --- a/@commitlint/read/src/read.ts +++ b/@commitlint/read/src/read.ts @@ -9,6 +9,7 @@ import {execa} from 'execa'; interface GetCommitMessageOptions { cwd?: string; from?: string; + fromLastTag?: boolean; to?: string; last?: boolean; edit?: boolean | string; @@ -19,18 +20,19 @@ interface GetCommitMessageOptions { export default async function getCommitMessages( settings: GetCommitMessageOptions ): Promise { - const {cwd, from, to, last, edit, gitLogArgs} = settings; + const {cwd, fromLastTag, to, last, edit, gitLogArgs} = settings; + let from = settings.from; if (edit) { return getEditCommit(cwd, edit); } if (last) { - const gitCommandResult = await execa('git', [ - 'log', - '-1', - '--pretty=format:%B', - ]); + const gitCommandResult = await execa( + 'git', + ['log', '-1', '--pretty=format:%B'], + {cwd} + ); let output = gitCommandResult.stdout; // strip output of extra quotation marks ("") if (output[0] == '"' && output[output.length - 1] == '"') @@ -38,6 +40,34 @@ export default async function getCommitMessages( return [output]; } + if (!from && fromLastTag) { + const {stdout} = await execa( + 'git', + [ + 'describe', + '--abbrev=40', + '--always', + '--first-parent', + '--long', + '--tags', + ], + {cwd} + ); + + if (stdout.length === 40) { + // Hash only means no last tag. Use that as the from ref which + // results in a no-op. + from = stdout; + } else { + // Description will be in the format: --g + // Example: v3.2.0-11-g9057371a52adaae5180d93fe4d0bb808d874b9fb + // Minus zero based (1), dash (1), "g" prefix (1), hash (40) = -43 + const tagSlice = stdout.lastIndexOf('-', stdout.length - 43); + + from = stdout.slice(0, tagSlice); + } + } + let gitOptions: GitOptions = {from, to}; if (gitLogArgs) { gitOptions = { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 9ae669d2ae..3aead4ec83 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -22,6 +22,8 @@ Options: -H, --help-url help url in error message [string] -f, --from lower end of the commit range to lint; applies if edit=false [string] + --from-last-tag uses the last tag as the lower end of the commit range to + lint; applies if edit=false and from is not set [boolean] --git-log-args additional git log arguments as space separated string, example '--first-parent --cherry-pick' [string] -l, --last just analyze the last commit; applies if edit=false