Skip to content

Commit

Permalink
Feat: improve output (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
thekaveman authored Oct 17, 2024
2 parents 50c461e + dd0ce28 commit fab6a95
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 74 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ repos:
entry: conventional-pre-commit
language: python
stages: [commit-msg]
args: [--verbose]
42 changes: 27 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,33 +46,43 @@ Conventional Commit......................................................Failed
- duration: 0.07s
- exit code: 1
[Bad Commit message] >> add a new feature
[Bad commit message] >> add a new feature
Your commit message does not follow Conventional Commits formatting
https://www.conventionalcommits.org/
```

And with the `--verbose` arg:

Conventional Commits start with one of the below types, followed by a colon,
followed by the commit message:
```console
$ git commit -m "add a new feature"
build chore ci docs feat fix perf refactor revert style test
[INFO] Initializing environment for ....
Conventional Commit......................................................Failed
- hook id: conventional-pre-commit
- duration: 0.07s
- exit code: 1
Example commit message adding a feature:
[Bad commit message] >> add a new feature
Your commit message does not follow Conventional Commits formatting
https://www.conventionalcommits.org/
feat: implement new API
Conventional Commit messages follow a pattern like:
Example commit message fixing an issue:
type(scope): subject
fix: remove infinite loop
extended body
Example commit with scope in parentheses after the type for more context:
Please correct the following errors:
fix(account): remove infinite loop
- Expected value for 'type' but found none.
- Expected value for 'delim' but found none.
- Expected value for 'subject' but found none.
Example commit with a body:
Run:
fix: remove infinite loop
git commit --edit --file=.git/COMMIT_EDITMSG
Additional information on the issue caused by the infinite loop
to edit the commit message and retry the commit.
```

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

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

Supply arguments on the command-line, or via the pre-commit `hooks.args` property:
Expand Down
48 changes: 39 additions & 9 deletions conventional_pre_commit/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,52 @@ def conventional_types(types=[]):
return types


def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None):
def conventional_regex(types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None):
types = conventional_types(types)

types_pattern = f"^(?P<type>{r_types(types)})?"
scope_pattern = f"(?P<scope>{r_scope(optional_scope, scopes=scopes)})?"
delim_pattern = f"(?P<delim>{r_delim()})?"
subject_pattern = f"(?P<subject>{r_subject()})?"
body_pattern = f"(?P<body>{r_body()})?"
pattern = types_pattern + scope_pattern + delim_pattern + subject_pattern + body_pattern

return re.compile(pattern, re.MULTILINE)


def clean_input(input: str):
"""
Prepares an input message for conventional commits format check.
"""
input = strip_verbose_commit_ignored(input)
input = strip_comments(input)
return input


def conventional_match(input: str, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None):
"""
Returns an `re.Match` object for the input against the Conventional Commits format.
"""
input = clean_input(input)
regex = conventional_regex(types, optional_scope, scopes)
return regex.match(input)


def is_conventional(input: str, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None) -> bool:
"""
Returns True if input matches Conventional Commits formatting
https://www.conventionalcommits.org
Optionally provide a list of additional custom types.
"""
input = strip_verbose_commit_ignored(input)
input = strip_comments(input)
types = conventional_types(types)
pattern = f"^({r_types(types)}){r_scope(optional_scope, scopes=scopes)}{r_delim()}{r_subject()}{r_body()}"
regex = re.compile(pattern, re.MULTILINE)

result = regex.match(input)
result = conventional_match(input, types, optional_scope, scopes)
is_valid = bool(result)
if is_valid and result.group("multi") and not result.group("sep"):

if result and result.group("multi") and not result.group("sep"):
is_valid = False
if result and not all(
[result.group("type"), optional_scope or result.group("scope"), result.group("delim"), result.group("subject")]
):
is_valid = False

return is_valid
Expand Down
72 changes: 23 additions & 49 deletions conventional_pre_commit/hook.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import argparse
import sys

from conventional_pre_commit import format
from conventional_pre_commit import format, output

RESULT_SUCCESS = 0
RESULT_FAIL = 1


class Colors:
LBLUE = "\033[00;34m"
LRED = "\033[01;31m"
RESTORE = "\033[0m"
YELLOW = "\033[00;33m"


def main(argv=[]):
parser = argparse.ArgumentParser(
prog="conventional-pre-commit", description="Check a git commit message for Conventional Commits formatting."
)
parser.add_argument("types", type=str, nargs="*", default=format.DEFAULT_TYPES, help="Optional list of types to support")
parser.add_argument("input", type=str, help="A file containing a git commit message")
parser.add_argument("--no-color", action="store_false", default=True, dest="color", help="Disable color in output.")
parser.add_argument(
"--force-scope", action="store_false", default=True, dest="optional_scope", help="Force commit to have scope defined."
)
Expand All @@ -34,6 +28,13 @@ def main(argv=[]):
action="store_true",
help="Force commit to strictly follow Conventional Commits formatting. Disallows fixup! style commits.",
)
parser.add_argument(
"--verbose",
action="store_true",
dest="verbose",
default=False,
help="Print more verbose error output.",
)

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

try:
with open(args.input, encoding="utf-8") as f:
message = f.read()
commit_msg = f.read()
except UnicodeDecodeError:
print(
f"""
{Colors.LRED}[Bad Commit message encoding] {Colors.RESTORE}
{Colors.YELLOW}conventional-pre-commit couldn't decode your commit message.{Colors.RESTORE}
{Colors.YELLOW}UTF-8{Colors.RESTORE} encoding is assumed, please configure git to write commit messages in UTF-8.
See {Colors.LBLUE}https://git-scm.com/docs/git-commit/#_discussion{Colors.RESTORE} for more.
"""
)
print(output.unicode_decode_error(args.color))
return RESULT_FAIL
if args.scopes:
scopes = args.scopes.split(",")
else:
scopes = args.scopes

if not args.strict:
if format.has_autosquash_prefix(message):
if format.has_autosquash_prefix(commit_msg):
return RESULT_SUCCESS

if format.is_conventional(message, args.types, args.optional_scope, scopes):
if format.is_conventional(commit_msg, args.types, args.optional_scope, scopes):
return RESULT_SUCCESS
else:
print(
f"""
{Colors.LRED}[Bad Commit message] >>{Colors.RESTORE} {message}
{Colors.YELLOW}Your commit message does not follow Conventional Commits formatting
{Colors.LBLUE}https://www.conventionalcommits.org/{Colors.YELLOW}
Conventional Commits start with one of the below types, followed by a colon,
followed by the commit subject and an optional body seperated by a blank line:{Colors.RESTORE}
{" ".join(format.conventional_types(args.types))}

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

feat: implement new API
{Colors.YELLOW}Example commit message fixing an issue:{Colors.RESTORE}
fix: remove infinite loop
{Colors.YELLOW}Example commit with scope in parentheses after the type for more context:{Colors.RESTORE}
fix(account): remove infinite loop
{Colors.YELLOW}Example commit with a body:{Colors.RESTORE}
fix: remove infinite loop
Additional information on the issue caused by the infinite loop
"""
if not args.verbose:
print(output.verbose_arg(use_color=args.color))
else:
print(
output.fail_verbose(
commit_msg, types=args.types, optional_scope=args.optional_scope, scopes=scopes, use_color=args.color
)
)
return RESULT_FAIL

return RESULT_FAIL


if __name__ == "__main__":
Expand Down
111 changes: 111 additions & 0 deletions conventional_pre_commit/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import os
from typing import List, Optional

from conventional_pre_commit import format


class Colors:
LBLUE = "\033[00;34m"
LRED = "\033[01;31m"
RESTORE = "\033[0m"
YELLOW = "\033[00;33m"

def __init__(self, enabled=True):
self.enabled = enabled

@property
def blue(self):
return self.LBLUE if self.enabled else ""

@property
def red(self):
return self.LRED if self.enabled else ""

@property
def restore(self):
return self.RESTORE if self.enabled else ""

@property
def yellow(self):
return self.YELLOW if self.enabled else ""


def fail(commit_msg, use_color=True):
c = Colors(use_color)
lines = [
f"{c.red}[Bad commit message] >>{c.restore} {commit_msg}"
f"{c.yellow}Your commit message does not follow Conventional Commits formatting{c.restore}",
f"{c.blue}https://www.conventionalcommits.org/{c.restore}",
]
return os.linesep.join(lines)


def verbose_arg(use_color=True):
c = Colors(use_color)
lines = [
"",
f"{c.yellow}Use the {c.restore}--verbose{c.yellow} arg for more information{c.restore}",
]
return os.linesep.join(lines)


def fail_verbose(
commit_msg: str, types=format.DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None, use_color=True
):
c = Colors(use_color)
match = format.conventional_match(commit_msg, types, optional_scope, scopes)
lines = [
"",
f"{c.yellow}Conventional Commit messages follow a pattern like:",
"",
f"{c.restore} type(scope): subject",
"",
" extended body",
"",
]

groups = match.groupdict() if match else {}

if optional_scope:
groups.pop("scope", None)

if not groups.get("body"):
groups.pop("body", None)
groups.pop("multi", None)
groups.pop("sep", None)

if groups.keys():
lines.append(f"{c.yellow}Please correct the following errors:{c.restore}")
lines.append("")
for group in [g for g, v in groups.items() if not v]:
if group == "scope":
if scopes:
scopt_opts = f"{c.yellow},{c.restore}".join(scopes)
lines.append(f"{c.yellow} - Expected value for {c.restore}scope{c.yellow} from: {c.restore}{scopt_opts}")
else:
lines.append(f"{c.yellow} - Expected value for {c.restore}scope{c.yellow} but found none.{c.restore}")
else:
lines.append(f"{c.yellow} - Expected value for {c.restore}{group}{c.yellow} but found none.{c.restore}")

lines.extend(
[
"",
f"{c.yellow}Run:{c.restore}",
"",
" git commit --edit --file=.git/COMMIT_EDITMSG",
"",
f"{c.yellow}to edit the commit message and retry the commit.{c.restore}",
]
)
return os.linesep.join(lines)


def unicode_decode_error(use_color=True):
c = Colors(use_color)
return f"""
{c.red}[Bad commit message encoding]{c.restore}
{c.yellow}conventional-pre-commit couldn't decode your commit message.
UTF-8 encoding is assumed, please configure git to write commit messages in UTF-8.
See {c.blue}https://git-scm.com/docs/git-commit/#_discussion{c.yellow} for more.{c.restore}
"""
2 changes: 1 addition & 1 deletion tests/messages/bad_commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bad: message
bad message
Loading

0 comments on commit fab6a95

Please sign in to comment.