Skip to content

Commit fab6a95

Browse files
authored
Feat: improve output (#117)
2 parents 50c461e + dd0ce28 commit fab6a95

File tree

9 files changed

+385
-74
lines changed

9 files changed

+385
-74
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ repos:
5151
entry: conventional-pre-commit
5252
language: python
5353
stages: [commit-msg]
54+
args: [--verbose]

README.md

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,33 +46,43 @@ Conventional Commit......................................................Failed
4646
- duration: 0.07s
4747
- exit code: 1
4848
49-
[Bad Commit message] >> add a new feature
50-
49+
[Bad commit message] >> add a new feature
5150
Your commit message does not follow Conventional Commits formatting
5251
https://www.conventionalcommits.org/
52+
```
53+
54+
And with the `--verbose` arg:
5355

54-
Conventional Commits start with one of the below types, followed by a colon,
55-
followed by the commit message:
56+
```console
57+
$ git commit -m "add a new feature"
5658
57-
build chore ci docs feat fix perf refactor revert style test
59+
[INFO] Initializing environment for ....
60+
Conventional Commit......................................................Failed
61+
- hook id: conventional-pre-commit
62+
- duration: 0.07s
63+
- exit code: 1
5864
59-
Example commit message adding a feature:
65+
[Bad commit message] >> add a new feature
66+
Your commit message does not follow Conventional Commits formatting
67+
https://www.conventionalcommits.org/
6068
61-
feat: implement new API
69+
Conventional Commit messages follow a pattern like:
6270
63-
Example commit message fixing an issue:
71+
type(scope): subject
6472
65-
fix: remove infinite loop
73+
extended body
6674
67-
Example commit with scope in parentheses after the type for more context:
75+
Please correct the following errors:
6876
69-
fix(account): remove infinite loop
77+
- Expected value for 'type' but found none.
78+
- Expected value for 'delim' but found none.
79+
- Expected value for 'subject' but found none.
7080
71-
Example commit with a body:
81+
Run:
7282
73-
fix: remove infinite loop
83+
git commit --edit --file=.git/COMMIT_EDITMSG
7484
75-
Additional information on the issue caused by the infinite loop
85+
to edit the commit message and retry the commit.
7686
```
7787

7888
Make a (conventional) commit :heavy_check_mark::
@@ -129,7 +139,7 @@ print(is_conventional("custom: this is a conventional commit", types=["custom"])
129139

130140
```shell
131141
$ conventional-pre-commit -h
132-
usage: conventional-pre-commit [-h] [--force-scope] [--scopes SCOPES] [--strict] [types ...] input
142+
usage: conventional-pre-commit [-h] [--no-color] [--force-scope] [--scopes SCOPES] [--strict] [--verbose] [types ...] input
133143
134144
Check a git commit message for Conventional Commits formatting.
135145
@@ -139,9 +149,11 @@ positional arguments:
139149
140150
options:
141151
-h, --help show this help message and exit
152+
--no-color Disable color in output.
142153
--force-scope Force commit to have scope defined.
143154
--scopes SCOPES Optional list of scopes to support. Scopes should be separated by commas with no spaces (e.g. api,client)
144155
--strict Force commit to strictly follow Conventional Commits formatting. Disallows fixup! style commits.
156+
--verbose Print more verbose error output.
145157
```
146158

147159
Supply arguments on the command-line, or via the pre-commit `hooks.args` property:

conventional_pre_commit/format.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,52 @@ def conventional_types(types=[]):
9696
return types
9797

9898

99-
def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None):
99+
def conventional_regex(types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None):
100+
types = conventional_types(types)
101+
102+
types_pattern = f"^(?P<type>{r_types(types)})?"
103+
scope_pattern = f"(?P<scope>{r_scope(optional_scope, scopes=scopes)})?"
104+
delim_pattern = f"(?P<delim>{r_delim()})?"
105+
subject_pattern = f"(?P<subject>{r_subject()})?"
106+
body_pattern = f"(?P<body>{r_body()})?"
107+
pattern = types_pattern + scope_pattern + delim_pattern + subject_pattern + body_pattern
108+
109+
return re.compile(pattern, re.MULTILINE)
110+
111+
112+
def clean_input(input: str):
113+
"""
114+
Prepares an input message for conventional commits format check.
115+
"""
116+
input = strip_verbose_commit_ignored(input)
117+
input = strip_comments(input)
118+
return input
119+
120+
121+
def conventional_match(input: str, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None):
122+
"""
123+
Returns an `re.Match` object for the input against the Conventional Commits format.
124+
"""
125+
input = clean_input(input)
126+
regex = conventional_regex(types, optional_scope, scopes)
127+
return regex.match(input)
128+
129+
130+
def is_conventional(input: str, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None) -> bool:
100131
"""
101132
Returns True if input matches Conventional Commits formatting
102133
https://www.conventionalcommits.org
103134
104135
Optionally provide a list of additional custom types.
105136
"""
106-
input = strip_verbose_commit_ignored(input)
107-
input = strip_comments(input)
108-
types = conventional_types(types)
109-
pattern = f"^({r_types(types)}){r_scope(optional_scope, scopes=scopes)}{r_delim()}{r_subject()}{r_body()}"
110-
regex = re.compile(pattern, re.MULTILINE)
111-
112-
result = regex.match(input)
137+
result = conventional_match(input, types, optional_scope, scopes)
113138
is_valid = bool(result)
114-
if is_valid and result.group("multi") and not result.group("sep"):
139+
140+
if result and result.group("multi") and not result.group("sep"):
141+
is_valid = False
142+
if result and not all(
143+
[result.group("type"), optional_scope or result.group("scope"), result.group("delim"), result.group("subject")]
144+
):
115145
is_valid = False
116146

117147
return is_valid

conventional_pre_commit/hook.py

Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
import argparse
22
import sys
33

4-
from conventional_pre_commit import format
4+
from conventional_pre_commit import format, output
55

66
RESULT_SUCCESS = 0
77
RESULT_FAIL = 1
88

99

10-
class Colors:
11-
LBLUE = "\033[00;34m"
12-
LRED = "\033[01;31m"
13-
RESTORE = "\033[0m"
14-
YELLOW = "\033[00;33m"
15-
16-
1710
def main(argv=[]):
1811
parser = argparse.ArgumentParser(
1912
prog="conventional-pre-commit", description="Check a git commit message for Conventional Commits formatting."
2013
)
2114
parser.add_argument("types", type=str, nargs="*", default=format.DEFAULT_TYPES, help="Optional list of types to support")
2215
parser.add_argument("input", type=str, help="A file containing a git commit message")
16+
parser.add_argument("--no-color", action="store_false", default=True, dest="color", help="Disable color in output.")
2317
parser.add_argument(
2418
"--force-scope", action="store_false", default=True, dest="optional_scope", help="Force commit to have scope defined."
2519
)
@@ -34,6 +28,13 @@ def main(argv=[]):
3428
action="store_true",
3529
help="Force commit to strictly follow Conventional Commits formatting. Disallows fixup! style commits.",
3630
)
31+
parser.add_argument(
32+
"--verbose",
33+
action="store_true",
34+
dest="verbose",
35+
default=False,
36+
help="Print more verbose error output.",
37+
)
3738

3839
if len(argv) < 1:
3940
argv = sys.argv[1:]
@@ -45,61 +46,34 @@ def main(argv=[]):
4546

4647
try:
4748
with open(args.input, encoding="utf-8") as f:
48-
message = f.read()
49+
commit_msg = f.read()
4950
except UnicodeDecodeError:
50-
print(
51-
f"""
52-
{Colors.LRED}[Bad Commit message encoding] {Colors.RESTORE}
53-
54-
{Colors.YELLOW}conventional-pre-commit couldn't decode your commit message.{Colors.RESTORE}
55-
{Colors.YELLOW}UTF-8{Colors.RESTORE} encoding is assumed, please configure git to write commit messages in UTF-8.
56-
See {Colors.LBLUE}https://git-scm.com/docs/git-commit/#_discussion{Colors.RESTORE} for more.
57-
"""
58-
)
51+
print(output.unicode_decode_error(args.color))
5952
return RESULT_FAIL
6053
if args.scopes:
6154
scopes = args.scopes.split(",")
6255
else:
6356
scopes = args.scopes
6457

6558
if not args.strict:
66-
if format.has_autosquash_prefix(message):
59+
if format.has_autosquash_prefix(commit_msg):
6760
return RESULT_SUCCESS
6861

69-
if format.is_conventional(message, args.types, args.optional_scope, scopes):
62+
if format.is_conventional(commit_msg, args.types, args.optional_scope, scopes):
7063
return RESULT_SUCCESS
71-
else:
72-
print(
73-
f"""
74-
{Colors.LRED}[Bad Commit message] >>{Colors.RESTORE} {message}
75-
{Colors.YELLOW}Your commit message does not follow Conventional Commits formatting
76-
{Colors.LBLUE}https://www.conventionalcommits.org/{Colors.YELLOW}
77-
78-
Conventional Commits start with one of the below types, followed by a colon,
79-
followed by the commit subject and an optional body seperated by a blank line:{Colors.RESTORE}
80-
81-
{" ".join(format.conventional_types(args.types))}
8264

83-
{Colors.YELLOW}Example commit message adding a feature:{Colors.RESTORE}
65+
print(output.fail(commit_msg, use_color=args.color))
8466

85-
feat: implement new API
86-
87-
{Colors.YELLOW}Example commit message fixing an issue:{Colors.RESTORE}
88-
89-
fix: remove infinite loop
90-
91-
{Colors.YELLOW}Example commit with scope in parentheses after the type for more context:{Colors.RESTORE}
92-
93-
fix(account): remove infinite loop
94-
95-
{Colors.YELLOW}Example commit with a body:{Colors.RESTORE}
96-
97-
fix: remove infinite loop
98-
99-
Additional information on the issue caused by the infinite loop
100-
"""
67+
if not args.verbose:
68+
print(output.verbose_arg(use_color=args.color))
69+
else:
70+
print(
71+
output.fail_verbose(
72+
commit_msg, types=args.types, optional_scope=args.optional_scope, scopes=scopes, use_color=args.color
73+
)
10174
)
102-
return RESULT_FAIL
75+
76+
return RESULT_FAIL
10377

10478

10579
if __name__ == "__main__":

conventional_pre_commit/output.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import os
2+
from typing import List, Optional
3+
4+
from conventional_pre_commit import format
5+
6+
7+
class Colors:
8+
LBLUE = "\033[00;34m"
9+
LRED = "\033[01;31m"
10+
RESTORE = "\033[0m"
11+
YELLOW = "\033[00;33m"
12+
13+
def __init__(self, enabled=True):
14+
self.enabled = enabled
15+
16+
@property
17+
def blue(self):
18+
return self.LBLUE if self.enabled else ""
19+
20+
@property
21+
def red(self):
22+
return self.LRED if self.enabled else ""
23+
24+
@property
25+
def restore(self):
26+
return self.RESTORE if self.enabled else ""
27+
28+
@property
29+
def yellow(self):
30+
return self.YELLOW if self.enabled else ""
31+
32+
33+
def fail(commit_msg, use_color=True):
34+
c = Colors(use_color)
35+
lines = [
36+
f"{c.red}[Bad commit message] >>{c.restore} {commit_msg}"
37+
f"{c.yellow}Your commit message does not follow Conventional Commits formatting{c.restore}",
38+
f"{c.blue}https://www.conventionalcommits.org/{c.restore}",
39+
]
40+
return os.linesep.join(lines)
41+
42+
43+
def verbose_arg(use_color=True):
44+
c = Colors(use_color)
45+
lines = [
46+
"",
47+
f"{c.yellow}Use the {c.restore}--verbose{c.yellow} arg for more information{c.restore}",
48+
]
49+
return os.linesep.join(lines)
50+
51+
52+
def fail_verbose(
53+
commit_msg: str, types=format.DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None, use_color=True
54+
):
55+
c = Colors(use_color)
56+
match = format.conventional_match(commit_msg, types, optional_scope, scopes)
57+
lines = [
58+
"",
59+
f"{c.yellow}Conventional Commit messages follow a pattern like:",
60+
"",
61+
f"{c.restore} type(scope): subject",
62+
"",
63+
" extended body",
64+
"",
65+
]
66+
67+
groups = match.groupdict() if match else {}
68+
69+
if optional_scope:
70+
groups.pop("scope", None)
71+
72+
if not groups.get("body"):
73+
groups.pop("body", None)
74+
groups.pop("multi", None)
75+
groups.pop("sep", None)
76+
77+
if groups.keys():
78+
lines.append(f"{c.yellow}Please correct the following errors:{c.restore}")
79+
lines.append("")
80+
for group in [g for g, v in groups.items() if not v]:
81+
if group == "scope":
82+
if scopes:
83+
scopt_opts = f"{c.yellow},{c.restore}".join(scopes)
84+
lines.append(f"{c.yellow} - Expected value for {c.restore}scope{c.yellow} from: {c.restore}{scopt_opts}")
85+
else:
86+
lines.append(f"{c.yellow} - Expected value for {c.restore}scope{c.yellow} but found none.{c.restore}")
87+
else:
88+
lines.append(f"{c.yellow} - Expected value for {c.restore}{group}{c.yellow} but found none.{c.restore}")
89+
90+
lines.extend(
91+
[
92+
"",
93+
f"{c.yellow}Run:{c.restore}",
94+
"",
95+
" git commit --edit --file=.git/COMMIT_EDITMSG",
96+
"",
97+
f"{c.yellow}to edit the commit message and retry the commit.{c.restore}",
98+
]
99+
)
100+
return os.linesep.join(lines)
101+
102+
103+
def unicode_decode_error(use_color=True):
104+
c = Colors(use_color)
105+
return f"""
106+
{c.red}[Bad commit message encoding]{c.restore}
107+
108+
{c.yellow}conventional-pre-commit couldn't decode your commit message.
109+
UTF-8 encoding is assumed, please configure git to write commit messages in UTF-8.
110+
See {c.blue}https://git-scm.com/docs/git-commit/#_discussion{c.yellow} for more.{c.restore}
111+
"""

tests/messages/bad_commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
bad: message
1+
bad message

0 commit comments

Comments
 (0)