-
Notifications
You must be signed in to change notification settings - Fork 919
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rules): add
trailer-exists
rule (#2578)
This new rule behaves similarly to the existing `signed-off-by` rule, but introduces what would otherwise be breaking changes to the parser in order to resolve some issues. Rather than attempting to parse the trailers manually, this rule will use Git's `interpret-trailers` subcommand to parse trailers according to Git's rules. This allows us to properly detect all valid trailers, and does not require that the trailer to search for be the last line of the commit message (as is required by the `signed-off-by` rule). One downside to this approach is that Git will not detect trailers if they are grouped alongside other trailers with whitespace in the token (e.g. `BREAKING CHANGE`). Projects that wish to use this rule may use `BREAKING-CHANGE`, which is synonymous with `BREAKING CHANGE` but is considered a valid trailer. Co-authored-by: Ade Attwood <[email protected]>
- Loading branch information
1 parent
4db4ba1
commit cd3816d
Showing
6 changed files
with
179 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', | ||
]), | ||
]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters