Skip to content

Commit e81cc87

Browse files
authored
Feat: check commits by command (closes #88) (#90)
1 parent 1ef051d commit e81cc87

File tree

12 files changed

+616
-8
lines changed

12 files changed

+616
-8
lines changed

.eslintrc.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,33 @@ module.exports = {
1616
ecmaVersion: 2018,
1717
sourceType: 'module',
1818
},
19+
settings: {
20+
'import/resolver': {
21+
node: {
22+
extensions: ['.js', '.ts'],
23+
},
24+
},
25+
},
26+
overrides: [
27+
{
28+
files: ['*.ts', '*.tsx'],
29+
rules: {
30+
'no-dupe-class-members': 'off',
31+
},
32+
},
33+
],
1934
plugins: [
2035
'@typescript-eslint',
2136
],
2237
rules: {
23-
},
38+
'@typescript-eslint/explicit-function-return-type': ['warn', {
39+
allowTypedFunctionExpressions: true,
40+
allowExpressions: true,
41+
}],
42+
'import/extensions': ['error', 'ignorePackages', {
43+
js: 'never',
44+
ts: 'never',
45+
}],
46+
'no-shadow-restricted-names': 'off',
47+
}
2448
};

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ Then, update the `semantic-release ` script to your `package.json` to this :
117117
}
118118
```
119119

120+
## Commands
121+
122+
### check
123+
124+
This will check all commits and will fail if your commits do not meet the defined config.
125+
126+
**Flags**
127+
- `start`: A commit SHA to start, in case you started using `sgc` later of your development
128+
129+
```sh
130+
$ sgc check --start 84a1abd
131+
```
132+
120133
## Config
121134

122135
> Just create a `.sgcrc` in your project root or you can add everything in your `package.json` with the value `sgc`

__tests__/check.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import gitCommitRange from 'git-commit-range';
2+
import chalk from 'chalk';
3+
4+
import check from '../lib/check';
5+
6+
jest.mock('git-commit-range', jest.fn);
7+
8+
const gitCommitRangeMock = gitCommitRange as jest.MockedFunction<typeof gitCommitRange>;
9+
10+
describe('check', () => {
11+
beforeEach(() => {
12+
global.process.exit = jest.fn() as any;
13+
global.console.error = jest.fn() as any;
14+
15+
chalk.enabled = false;
16+
});
17+
18+
it('should have valid commits', () => {
19+
gitCommitRangeMock.mockReturnValue([
20+
'Feat: should be valid',
21+
]);
22+
23+
check();
24+
25+
expect(process.exit).toBeCalledTimes(1);
26+
expect(process.exit).toBeCalledWith(0);
27+
28+
check();
29+
30+
expect(process.exit).toBeCalledTimes(2);
31+
expect(process.exit).toBeCalledWith(0);
32+
});
33+
34+
it('should have valid initial commits', () => {
35+
gitCommitRangeMock.mockReturnValue([
36+
'Feat: should be valid',
37+
'Initial commit',
38+
]);
39+
40+
check();
41+
42+
expect(process.exit).toBeCalledTimes(1);
43+
expect(process.exit).toBeCalledWith(0);
44+
});
45+
46+
it('should have invalid commits', () => {
47+
gitCommitRangeMock.mockReturnValue([
48+
'NotValid : should be valid',
49+
]);
50+
51+
check();
52+
53+
expect(process.exit).toBeCalledTimes(1);
54+
expect(process.exit).toBeCalledWith(1);
55+
// eslint-disable-next-line no-console
56+
expect(console.error).toBeCalledWith('\nNotVali\nNotValid : should be valid');
57+
58+
check();
59+
60+
expect(process.exit).toBeCalledTimes(2);
61+
expect(process.exit).toBeCalledWith(1);
62+
});
63+
});
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import getConfig from '../../lib/getConfig';
2+
import commitMeetsRules from '../../lib/helpers/commitMeetsRules';
3+
4+
jest.mock('../../lib/getConfig', jest.fn);
5+
6+
const getConfigMock = getConfig as jest.MockedFunction<typeof getConfig>;
7+
8+
describe('commitMeetsRules', () => {
9+
it('should have one of the types', () => {
10+
getConfigMock.mockReturnValue({
11+
types: [
12+
{ type: 'Chore' },
13+
],
14+
});
15+
16+
expect(commitMeetsRules('Feat: false')).toBe(false);
17+
expect(commitMeetsRules('Chore: true')).toBe(true);
18+
expect(commitMeetsRules('Chore : true')).toBe(false);
19+
});
20+
21+
it('should have one of the types', () => {
22+
getConfigMock.mockReturnValue({
23+
lowercaseTypes: true,
24+
types: [
25+
{ type: 'Chore' },
26+
],
27+
});
28+
29+
expect(commitMeetsRules('Feat: false')).toBe(false);
30+
expect(commitMeetsRules('feat: false')).toBe(false);
31+
expect(commitMeetsRules('chore: true')).toBe(true);
32+
expect(commitMeetsRules('Chore: true')).toBe(false);
33+
expect(commitMeetsRules('chore : true')).toBe(false);
34+
});
35+
36+
it('should have one of the types with different delimiter', () => {
37+
getConfigMock.mockReturnValue({
38+
delimiter: ' -',
39+
types: [
40+
{ type: 'Chore' },
41+
{ type: 'Fix' },
42+
],
43+
});
44+
45+
expect(commitMeetsRules('Feat - false')).toBe(false);
46+
expect(commitMeetsRules('Fix - false')).toBe(true);
47+
expect(commitMeetsRules('Chore - true')).toBe(true);
48+
expect(commitMeetsRules('Chore : true')).toBe(false);
49+
});
50+
51+
it('should not have scope', () => {
52+
getConfigMock.mockReturnValue({
53+
scope: false,
54+
types: [
55+
{ type: 'Feat' },
56+
],
57+
});
58+
59+
expect(commitMeetsRules('Feat(scope): Test')).toBe(false);
60+
expect(commitMeetsRules('Feat (scope): Test')).toBe(false);
61+
expect(commitMeetsRules('Feat (scope) : Test')).toBe(false);
62+
expect(commitMeetsRules('Feat: Test')).toBe(true);
63+
expect(commitMeetsRules('Feat: Test (scope at the end)')).toBe(true);
64+
expect(commitMeetsRules('Feat : Test')).toBe(false);
65+
expect(commitMeetsRules('Feat: Test ')).toBe(true);
66+
expect(commitMeetsRules('Feat : Test')).toBe(false);
67+
});
68+
69+
it('should have optional scope', () => {
70+
getConfigMock.mockReturnValue({
71+
scope: true,
72+
types: [
73+
{ type: 'Feat' },
74+
],
75+
});
76+
77+
expect(commitMeetsRules('Feat(scope): Test')).toBe(true);
78+
expect(commitMeetsRules('Feat (scope): Test')).toBe(false);
79+
expect(commitMeetsRules('Feat (scope) : Test')).toBe(false);
80+
expect(commitMeetsRules('Feat: Test')).toBe(true);
81+
expect(commitMeetsRules('Feat : Test')).toBe(false);
82+
expect(commitMeetsRules('Feat: Test ')).toBe(true);
83+
expect(commitMeetsRules('Feat : Test')).toBe(false);
84+
});
85+
86+
it('should have optional scope with scopespace', () => {
87+
getConfigMock.mockReturnValue({
88+
scope: true,
89+
addScopeSpace: true,
90+
types: [
91+
{ type: 'Feat' },
92+
],
93+
});
94+
95+
expect(commitMeetsRules('Feat(scope): Test')).toBe(false);
96+
expect(commitMeetsRules('Feat (scope): Test')).toBe(true);
97+
expect(commitMeetsRules('Feat (scope) : Test')).toBe(false);
98+
expect(commitMeetsRules('Feat: Test')).toBe(true);
99+
expect(commitMeetsRules('Feat : Test')).toBe(false);
100+
expect(commitMeetsRules('Feat: Test ')).toBe(true);
101+
expect(commitMeetsRules('Feat : Test')).toBe(false);
102+
});
103+
104+
it('should have dot ending', () => {
105+
getConfigMock.mockReturnValue({
106+
rules: {
107+
endWithDot: true,
108+
},
109+
types: [
110+
{ type: 'Feat' },
111+
],
112+
});
113+
114+
expect(commitMeetsRules('Feat: Test.')).toBe(true);
115+
expect(commitMeetsRules('Feat: Test')).toBe(false);
116+
expect(commitMeetsRules('Feat : Test.')).toBe(false);
117+
expect(commitMeetsRules('Feat : Test')).toBe(false);
118+
});
119+
120+
it('should have no dot ending', () => {
121+
getConfigMock.mockReturnValue({
122+
rules: {
123+
endWithDot: false,
124+
},
125+
types: [
126+
{ type: 'Feat' },
127+
],
128+
});
129+
130+
expect(commitMeetsRules('Feat: Test')).toBe(true);
131+
expect(commitMeetsRules('Feat: Test.')).toBe(false);
132+
expect(commitMeetsRules('Feat : Test.')).toBe(false);
133+
expect(commitMeetsRules('Feat : Test')).toBe(false);
134+
});
135+
136+
it('should have correct length', () => {
137+
getConfigMock.mockReturnValue({
138+
rules: {
139+
maxChar: 10,
140+
minChar: 8,
141+
},
142+
types: [
143+
{ type: 'Feat' },
144+
],
145+
});
146+
147+
expect(commitMeetsRules('Feat: T')).toBe(false);
148+
expect(commitMeetsRules('Feat: Te')).toBe(true);
149+
expect(commitMeetsRules('Feat: Tes')).toBe(true);
150+
expect(commitMeetsRules('Feat: Test')).toBe(true);
151+
expect(commitMeetsRules('Feat: Test1')).toBe(false);
152+
});
153+
154+
it('should have no length', () => {
155+
getConfigMock.mockReturnValue({
156+
types: [
157+
{ type: 'Feat' },
158+
],
159+
});
160+
161+
expect(commitMeetsRules('Feat: T')).toBe(true);
162+
expect(commitMeetsRules('Feat: Te')).toBe(true);
163+
expect(commitMeetsRules('Feat: Tes')).toBe(true);
164+
expect(commitMeetsRules('Feat: Test')).toBe(true);
165+
expect(commitMeetsRules('Feat: Test1')).toBe(true);
166+
});
167+
168+
it('should have body', () => {
169+
getConfigMock.mockReturnValue({
170+
body: true,
171+
types: [
172+
{ type: 'Feat' },
173+
],
174+
});
175+
176+
expect(commitMeetsRules('Chore: T\n\nmy body in here')).toBe(false);
177+
expect(commitMeetsRules('Feat: T\n\nmy body in here')).toBe(true);
178+
expect(commitMeetsRules('Feat: T\ninvalid body in here')).toBe(false);
179+
});
180+
181+
it('should have initial commit', () => {
182+
getConfigMock.mockReturnValue({
183+
initialCommit: {
184+
isEnabled: true,
185+
message: 'initial commit',
186+
},
187+
});
188+
189+
expect(commitMeetsRules('initial commit')).toBe(true);
190+
expect(commitMeetsRules('Initial commit')).toBe(false);
191+
192+
getConfigMock.mockReturnValue({
193+
initialCommit: {
194+
isEnabled: false,
195+
message: 'initial commit',
196+
},
197+
});
198+
199+
expect(commitMeetsRules('initial commit')).toBe(false);
200+
expect(commitMeetsRules('Initial commit')).toBe(false);
201+
});
202+
});

global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare module 'git-commit-range';
2+
declare module 'chalk';

lib/check.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import gitCommitRange from 'git-commit-range';
2+
import chalk from 'chalk';
3+
4+
import commitMeetsRules from './helpers/commitMeetsRules';
5+
6+
const check = ({ start }: { start?: string } = {}): void => {
7+
const commitRangeText: string[] = gitCommitRange({
8+
from: start,
9+
type: 'text',
10+
includeMerges: false,
11+
});
12+
const commitRangeSha: string[] = gitCommitRange({
13+
from: start,
14+
includeMerges: false,
15+
});
16+
17+
let hasErrors = false;
18+
19+
commitRangeText.forEach((commit, i) => {
20+
const isCommitValid = commitMeetsRules(commit);
21+
22+
if (!isCommitValid) {
23+
const commitSha = commitRangeSha[i].slice(0, 7);
24+
25+
hasErrors = true;
26+
27+
// eslint-disable-next-line no-console
28+
console.error(chalk.red(`\n${chalk.bold(commitSha)}\n${commit}`));
29+
}
30+
31+
return isCommitValid;
32+
});
33+
34+
process.exit(+hasErrors);
35+
};
36+
37+
export default check;

0 commit comments

Comments
 (0)