Skip to content

Commit 4b204ec

Browse files
authored
feat: support linting from the last tag (#4110)
* fix: observe working directory with --last option * feat: support linting from the last tag
1 parent a20e890 commit 4b204ec

File tree

6 files changed

+105
-6
lines changed

6 files changed

+105
-6
lines changed

@commitlint/cli/src/cli.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ test('should print help', async () => {
548548
-x, --extends array of shareable configurations to extend [array]
549549
-H, --help-url help url in error message [string]
550550
-f, --from lower end of the commit range to lint; applies if edit=false [string]
551+
--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]
551552
--git-log-args additional git log arguments as space separated string, example '--first-parent --cherry-pick' [string]
552553
-l, --last just analyze the last commit; applies if edit=false [boolean]
553554
-o, --format output format of the results [string]

@commitlint/cli/src/cli.ts

+7
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ const cli = yargs(process.argv.slice(2))
9090
'lower end of the commit range to lint; applies if edit=false',
9191
type: 'string',
9292
},
93+
'from-last-tag': {
94+
description:
95+
'uses the last tag as the lower end of the commit range to lint; applies if edit=false and from is not set',
96+
type: 'boolean',
97+
},
9398
'git-log-args': {
9499
description:
95100
"additional git log arguments as space separated string, example '--first-parent --cherry-pick'",
@@ -242,6 +247,7 @@ async function main(args: MainArgs): Promise<void> {
242247
: read({
243248
to: flags.to,
244249
from: flags.from,
250+
fromLastTag: flags['from-last-tag'],
245251
last: flags.last,
246252
edit: flags.edit,
247253
cwd: flags.cwd,
@@ -400,6 +406,7 @@ function checkFromEdit(flags: CliFlags): boolean {
400406
function checkFromHistory(flags: CliFlags): boolean {
401407
return (
402408
typeof flags.from === 'string' ||
409+
typeof flags['from-last-tag'] === 'boolean' ||
403410
typeof flags.to === 'string' ||
404411
typeof flags.last === 'boolean'
405412
);

@commitlint/cli/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface CliFlags {
88
help?: boolean;
99
'help-url'?: string;
1010
from?: string;
11+
'from-last-tag'?: boolean;
1112
'git-log-args'?: string;
1213
last?: boolean;
1314
format?: string;

@commitlint/read/src/read.test.ts

+58
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,61 @@ test('get edit commit message while skipping first commit', async () => {
7272
const actual = await read({from: 'HEAD~2', cwd, gitLogArgs: '--skip 1'});
7373
expect(actual).toEqual(expected);
7474
});
75+
76+
test('should only read the last commit', async () => {
77+
const cwd: string = await git.bootstrap();
78+
79+
await execa('git', ['commit', '--allow-empty', '-m', 'commit Z'], {cwd});
80+
await execa('git', ['commit', '--allow-empty', '-m', 'commit Y'], {cwd});
81+
await execa('git', ['commit', '--allow-empty', '-m', 'commit X'], {cwd});
82+
83+
const result = await read({cwd, last: true});
84+
85+
expect(result).toEqual(['commit X']);
86+
});
87+
88+
test('should read commits from the last annotated tag', async () => {
89+
const cwd: string = await git.bootstrap();
90+
91+
await execa(
92+
'git',
93+
['commit', '--allow-empty', '-m', 'chore: release v1.0.0'],
94+
{cwd}
95+
);
96+
await execa('git', ['tag', 'v1.0.0', '--annotate', '-m', 'v1.0.0'], {cwd});
97+
await execa('git', ['commit', '--allow-empty', '-m', 'commit 1'], {cwd});
98+
await execa('git', ['commit', '--allow-empty', '-m', 'commit 2'], {cwd});
99+
100+
const result = await read({cwd, fromLastTag: true});
101+
102+
expect(result).toEqual(['commit 2\n\n', 'commit 1\n\n']);
103+
});
104+
105+
test('should read commits from the last lightweight tag', async () => {
106+
const cwd: string = await git.bootstrap();
107+
108+
await execa(
109+
'git',
110+
['commit', '--allow-empty', '-m', 'chore: release v9.9.9-alpha.1'],
111+
{cwd}
112+
);
113+
await execa('git', ['tag', 'v9.9.9-alpha.1'], {cwd});
114+
await execa('git', ['commit', '--allow-empty', '-m', 'commit A'], {cwd});
115+
await execa('git', ['commit', '--allow-empty', '-m', 'commit B'], {cwd});
116+
117+
const result = await read({cwd, fromLastTag: true});
118+
119+
expect(result).toEqual(['commit B\n\n', 'commit A\n\n']);
120+
});
121+
122+
test('should not read any commits when there are no tags', async () => {
123+
const cwd: string = await git.bootstrap();
124+
125+
await execa('git', ['commit', '--allow-empty', '-m', 'commit 7'], {cwd});
126+
await execa('git', ['commit', '--allow-empty', '-m', 'commit 8'], {cwd});
127+
await execa('git', ['commit', '--allow-empty', '-m', 'commit 9'], {cwd});
128+
129+
const result = await read({cwd, fromLastTag: true});
130+
131+
expect(result).toHaveLength(0);
132+
});

@commitlint/read/src/read.ts

+36-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {execa} from 'execa';
99
interface GetCommitMessageOptions {
1010
cwd?: string;
1111
from?: string;
12+
fromLastTag?: boolean;
1213
to?: string;
1314
last?: boolean;
1415
edit?: boolean | string;
@@ -19,25 +20,54 @@ interface GetCommitMessageOptions {
1920
export default async function getCommitMessages(
2021
settings: GetCommitMessageOptions
2122
): Promise<string[]> {
22-
const {cwd, from, to, last, edit, gitLogArgs} = settings;
23+
const {cwd, fromLastTag, to, last, edit, gitLogArgs} = settings;
24+
let from = settings.from;
2325

2426
if (edit) {
2527
return getEditCommit(cwd, edit);
2628
}
2729

2830
if (last) {
29-
const gitCommandResult = await execa('git', [
30-
'log',
31-
'-1',
32-
'--pretty=format:%B',
33-
]);
31+
const gitCommandResult = await execa(
32+
'git',
33+
['log', '-1', '--pretty=format:%B'],
34+
{cwd}
35+
);
3436
let output = gitCommandResult.stdout;
3537
// strip output of extra quotation marks ("")
3638
if (output[0] == '"' && output[output.length - 1] == '"')
3739
output = output.slice(1, -1);
3840
return [output];
3941
}
4042

43+
if (!from && fromLastTag) {
44+
const {stdout} = await execa(
45+
'git',
46+
[
47+
'describe',
48+
'--abbrev=40',
49+
'--always',
50+
'--first-parent',
51+
'--long',
52+
'--tags',
53+
],
54+
{cwd}
55+
);
56+
57+
if (stdout.length === 40) {
58+
// Hash only means no last tag. Use that as the from ref which
59+
// results in a no-op.
60+
from = stdout;
61+
} else {
62+
// Description will be in the format: <tag>-<count>-g<hash>
63+
// Example: v3.2.0-11-g9057371a52adaae5180d93fe4d0bb808d874b9fb
64+
// Minus zero based (1), dash (1), "g" prefix (1), hash (40) = -43
65+
const tagSlice = stdout.lastIndexOf('-', stdout.length - 43);
66+
67+
from = stdout.slice(0, tagSlice);
68+
}
69+
}
70+
4171
let gitOptions: GitOptions = {from, to};
4272
if (gitLogArgs) {
4373
gitOptions = {

docs/reference/cli.md

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Options:
2222
-H, --help-url help url in error message [string]
2323
-f, --from lower end of the commit range to lint; applies if
2424
edit=false [string]
25+
--from-last-tag uses the last tag as the lower end of the commit range to
26+
lint; applies if edit=false and from is not set [boolean]
2527
--git-log-args additional git log arguments as space separated string,
2628
example '--first-parent --cherry-pick' [string]
2729
-l, --last just analyze the last commit; applies if edit=false

0 commit comments

Comments
 (0)