Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rules): add trailer-exists rule #2578

Merged
merged 2 commits into from May 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion @commitlint/rules/package.json
Expand Up @@ -44,7 +44,8 @@
"@commitlint/ensure": "^12.1.4",
"@commitlint/message": "^12.1.4",
"@commitlint/to-lines": "^12.1.4",
"@commitlint/types": "^12.1.4"
"@commitlint/types": "^12.1.4",
"execa": "^5.0.0"
},
"gitHead": "70f7f4688b51774e7ac5e40e896cdaa3f132b2bc"
}
2 changes: 2 additions & 0 deletions @commitlint/rules/src/index.ts
Expand Up @@ -26,6 +26,7 @@ import {subjectEmpty} from './subject-empty';
import {subjectFullStop} from './subject-full-stop';
import {subjectMaxLength} from './subject-max-length';
import {subjectMinLength} from './subject-min-length';
import {trailerExists} from './trailer-exists';
import {typeCase} from './type-case';
import {typeEmpty} from './type-empty';
import {typeEnum} from './type-enum';
Expand Down Expand Up @@ -61,6 +62,7 @@ export default {
'subject-full-stop': subjectFullStop,
'subject-max-length': subjectMaxLength,
'subject-min-length': subjectMinLength,
'trailer-exists': trailerExists,
'type-case': typeCase,
'type-empty': typeEmpty,
'type-enum': typeEnum,
Expand Down
136 changes: 136 additions & 0 deletions @commitlint/rules/src/trailer-exists.test.ts
@@ -0,0 +1,136 @@
import parse from '@commitlint/parse';
import {trailerExists} from './trailer-exists';

const messages = {
empty: 'test:\n',
with: `test: subject\n\nbody\n\nfooter\n\nSigned-off-by:\n\n`,
without: `test: subject\n\nbody\n\nfooter\n\n`,
inSubject: `test: subject Signed-off-by:\n\nbody\n\nfooter\n\n`,
inBody: `test: subject\n\nbody Signed-off-by:\n\nfooter\n\n`,
withSignoffAndNoise: `test: subject

message body

Arbitrary-trailer:
Signed-off-by:
Another-arbitrary-trailer:

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
`,
};

const parsed = {
empty: parse(messages.empty),
with: parse(messages.with),
without: parse(messages.without),
inSubject: parse(messages.inSubject),
inBody: parse(messages.inBody),
withSignoffAndNoise: parse(messages.withSignoffAndNoise),
};

test('empty against "always trailer-exists" should fail', async () => {
const [actual] = trailerExists(
await parsed.empty,
'always',
'Signed-off-by:'
);

const expected = false;
expect(actual).toEqual(expected);
});

test('empty against "never trailer-exists" should succeed', async () => {
const [actual] = trailerExists(await parsed.empty, 'never', 'Signed-off-by:');
const expected = true;
expect(actual).toEqual(expected);
});

test('with against "always trailer-exists" should succeed', async () => {
const [actual] = trailerExists(await parsed.with, 'always', 'Signed-off-by:');
const expected = true;
expect(actual).toEqual(expected);
});

test('with against "never trailer-exists" should fail', async () => {
const [actual] = trailerExists(await parsed.with, 'never', 'Signed-off-by:');
const expected = false;
expect(actual).toEqual(expected);
});

test('without against "always trailer-exists" should fail', async () => {
const [actual] = trailerExists(
await parsed.without,
'always',
'Signed-off-by:'
);

const expected = false;
expect(actual).toEqual(expected);
});

test('without against "never trailer-exists" should succeed', async () => {
const [actual] = trailerExists(
await parsed.without,
'never',
'Signed-off-by:'
);

const expected = true;
expect(actual).toEqual(expected);
});

test('comments and other trailers should be ignored', async () => {
const [actual] = trailerExists(
await parsed.withSignoffAndNoise,
'always',
'Signed-off-by:'
);

const expected = true;
expect(actual).toEqual(expected);
});

test('inSubject against "always trailer-exists" should fail', async () => {
const [actual] = trailerExists(
await parsed.inSubject,
'always',
'Signed-off-by:'
);

const expected = false;
expect(actual).toEqual(expected);
});

test('inSubject against "never trailer-exists" should succeed', async () => {
const [actual] = trailerExists(
await parsed.inSubject,
'never',
'Signed-off-by:'
);

const expected = true;
expect(actual).toEqual(expected);
});

test('inBody against "always trailer-exists" should fail', async () => {
const [actual] = trailerExists(
await parsed.inBody,
'always',
'Signed-off-by:'
);

const expected = false;
expect(actual).toEqual(expected);
});

test('inBody against "never trailer-exists" should succeed', async () => {
const [actual] = trailerExists(
await parsed.inBody,
'never',
'Signed-off-by:'
);

const expected = true;
expect(actual).toEqual(expected);
});
28 changes: 28 additions & 0 deletions @commitlint/rules/src/trailer-exists.ts
@@ -0,0 +1,28 @@
import execa from 'execa';
import message from '@commitlint/message';
import toLines from '@commitlint/to-lines';
import {SyncRule} from '@commitlint/types';

export const trailerExists: SyncRule<string> = (
parsed,
when = 'always',
value = ''
) => {
const trailers = execa.sync('git', ['interpret-trailers', '--parse'], {
input: parsed.raw,
}).stdout;

const matches = toLines(trailers).filter((ln) => ln.startsWith(value)).length;

const negated = when === 'never';
const hasTrailer = matches > 0;

return [
negated ? !hasTrailer : hasTrailer,
message([
'message',
negated ? 'must not' : 'must',
'have `' + value + '` trailer',
]),
];
};
1 change: 1 addition & 0 deletions @commitlint/types/src/rules.ts
Expand Up @@ -115,6 +115,7 @@ export type RulesConfig<V = RuleConfigQuality.User> = {
'subject-full-stop': RuleConfig<V, string>;
'subject-max-length': LengthRuleConfig<V>;
'subject-min-length': LengthRuleConfig<V>;
'trailer-exists': RuleConfig<V, string>;
'type-case': CaseRuleConfig<V>;
'type-empty': RuleConfig<V>;
'type-enum': EnumRuleConfig<V>;
Expand Down
10 changes: 10 additions & 0 deletions docs/reference-rules.md
Expand Up @@ -402,3 +402,13 @@ Infinity
```
'Signed-off-by:'
```

#### trailer-exists

- **condition**: `message` has trailer `value`
- **rule**: `always`
- **value**

```
'Signed-off-by:'
```