From 0f77fdc5a7ddec50be501fd26c52463360f1f3f7 Mon Sep 17 00:00:00 2001 From: Marten Ringwelski Date: Wed, 16 Oct 2024 17:30:31 +0200 Subject: [PATCH 01/10] feat: Give a hint on how to reedit the commit message I think it is not common knowledge that the commit message is stored in `.git/COMMIT_EDITMSG` if the hook fails. Let's give the user a hint and a copy-pastable command to retry the commit. --- conventional_pre_commit/hook.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conventional_pre_commit/hook.py b/conventional_pre_commit/hook.py index 25d6090..c63e6e3 100644 --- a/conventional_pre_commit/hook.py +++ b/conventional_pre_commit/hook.py @@ -75,6 +75,10 @@ def main(argv=[]): {Colors.YELLOW}Your commit message does not follow Conventional Commits formatting {Colors.LBLUE}https://www.conventionalcommits.org/{Colors.YELLOW} + Run + git commit --edit --file=.git/COMMIT_EDITMSG + to reedit the commit message do the commit. + 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} From 95565c2c9e8a9caecbd1f89a55f89daf7374d325 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 16 Oct 2024 21:59:41 +0000 Subject: [PATCH 02/10] feat(output): module to produce CLI messages --- conventional_pre_commit/output.py | 41 +++++++++++++++++++++++++++++++ tests/test_output.py | 25 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 conventional_pre_commit/output.py create mode 100644 tests/test_output.py diff --git a/conventional_pre_commit/output.py b/conventional_pre_commit/output.py new file mode 100644 index 0000000..e2f880d --- /dev/null +++ b/conventional_pre_commit/output.py @@ -0,0 +1,41 @@ +import os + + +class Colors: + LBLUE = "\033[00;34m" + LRED = "\033[01;31m" + RESTORE = "\033[0m" + YELLOW = "\033[00;33m" + + +def fail(commit_msg): + lines = [ + f"{Colors.LRED}[Bad commit message] >>{Colors.RESTORE} {commit_msg}" + f"{Colors.YELLOW}Your commit message does not follow Conventional Commits formatting{Colors.RESTORE}", + f"{Colors.LBLUE}https://www.conventionalcommits.org/{Colors.RESTORE}", + "", + f"{Colors.YELLOW}Use the {Colors.RESTORE}--verbose{Colors.YELLOW} arg for more information{Colors.RESTORE}", + ] + return os.linesep.join(lines) + + +def fail_verbose(commit_msg): + lines = [ + "", + f"{Colors.YELLOW}Run{Colors.RESTORE}", + "", + " git commit --edit --file=.git/COMMIT_EDITMSG", + "", + f"{Colors.YELLOW}to edit the commit message and retry the commit.{Colors.RESTORE}", + ] + return os.linesep.join(lines) + + +def unicode_decode_error(): + return f""" +{Colors.LRED}[Bad commit message encoding]{Colors.RESTORE} + +{Colors.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 {Colors.LBLUE}https://git-scm.com/docs/git-commit/#_discussion{Colors.YELLOW} for more.{Colors.RESTORE} +""" diff --git a/tests/test_output.py b/tests/test_output.py new file mode 100644 index 0000000..fd8f267 --- /dev/null +++ b/tests/test_output.py @@ -0,0 +1,25 @@ +from conventional_pre_commit.output import fail, fail_verbose, unicode_decode_error + + +def test_fail(): + output = fail("commit msg") + + assert "Bad commit message" in output + assert "commit msg" in output + assert "Conventional Commits formatting" in output + assert "https://www.conventionalcommits.org/" in output + + +def test_fail_verbose(): + output = fail_verbose("commit msg") + + assert "git commit --edit --file=.git/COMMIT_EDITMSG" in output + assert "edit the commit message and retry the commit" in output + + +def test_unicode_decode_error(): + output = unicode_decode_error() + + assert "Bad commit message encoding" in output + assert "UTF-8 encoding is assumed" in output + assert "https://git-scm.com/docs/git-commit/#_discussion" in output From beb5c03e48d3c381f62ffd729c98227f3ef60165 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 16 Oct 2024 22:02:37 +0000 Subject: [PATCH 03/10] refactor(hook): get output from helper functions --- conventional_pre_commit/hook.py | 61 ++++----------------------------- 1 file changed, 7 insertions(+), 54 deletions(-) diff --git a/conventional_pre_commit/hook.py b/conventional_pre_commit/hook.py index c63e6e3..4c05b96 100644 --- a/conventional_pre_commit/hook.py +++ b/conventional_pre_commit/hook.py @@ -1,19 +1,12 @@ 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." @@ -45,17 +38,9 @@ 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()) return RESULT_FAIL if args.scopes: scopes = args.scopes.split(",") @@ -63,47 +48,15 @@ def main(argv=[]): 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} - - Run - git commit --edit --file=.git/COMMIT_EDITMSG - to reedit the commit message do the commit. - - 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} - - feat: implement new API + print(output.fail(commit_msg)) - {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 - """ - ) - return RESULT_FAIL + return RESULT_FAIL if __name__ == "__main__": From 174d4468d03b2ee809f18b27c258d5cbe8879cef Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 16 Oct 2024 22:03:00 +0000 Subject: [PATCH 04/10] feat(hook): arg for more verbose output --- conventional_pre_commit/hook.py | 14 +++++++++++++- conventional_pre_commit/output.py | 6 ++++++ tests/messages/bad_commit | 2 +- tests/test_hook.py | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/conventional_pre_commit/hook.py b/conventional_pre_commit/hook.py index 4c05b96..2557031 100644 --- a/conventional_pre_commit/hook.py +++ b/conventional_pre_commit/hook.py @@ -27,6 +27,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:] @@ -53,8 +60,13 @@ def main(argv=[]): if format.is_conventional(commit_msg, args.types, args.optional_scope, scopes): return RESULT_SUCCESS + + print(output.fail(commit_msg)) + + if not args.verbose: + print(output.verbose_arg()) else: - print(output.fail(commit_msg)) + print(output.fail_verbose(commit_msg, args.types, args.optional_scope, scopes)) return RESULT_FAIL diff --git a/conventional_pre_commit/output.py b/conventional_pre_commit/output.py index e2f880d..0658805 100644 --- a/conventional_pre_commit/output.py +++ b/conventional_pre_commit/output.py @@ -13,6 +13,12 @@ def fail(commit_msg): f"{Colors.LRED}[Bad commit message] >>{Colors.RESTORE} {commit_msg}" f"{Colors.YELLOW}Your commit message does not follow Conventional Commits formatting{Colors.RESTORE}", f"{Colors.LBLUE}https://www.conventionalcommits.org/{Colors.RESTORE}", + ] + return os.linesep.join(lines) + + +def verbose_arg(): + lines = [ "", f"{Colors.YELLOW}Use the {Colors.RESTORE}--verbose{Colors.YELLOW} arg for more information{Colors.RESTORE}", ] diff --git a/tests/messages/bad_commit b/tests/messages/bad_commit index bb212db..75909c7 100644 --- a/tests/messages/bad_commit +++ b/tests/messages/bad_commit @@ -1 +1 @@ -bad: message +bad message diff --git a/tests/test_hook.py b/tests/test_hook.py index 59056bc..a452578 100644 --- a/tests/test_hook.py +++ b/tests/test_hook.py @@ -1,3 +1,4 @@ +import os import subprocess import pytest @@ -94,6 +95,24 @@ def test_main_fail__conventional_commit_bad_multi_line(conventional_commit_bad_m assert result == RESULT_FAIL +def test_main_fail__verbose(bad_commit_path, capsys): + result = main(["--verbose", "--force-scope", bad_commit_path]) + + assert result == RESULT_FAIL + + captured = capsys.readouterr() + output = captured.out + + assert "Conventional Commit messages follow a pattern like" in output + assert f"type(scope): subject{os.linesep}{os.linesep} extended body" in output + assert "Expected value for 'type' but found none." in output + assert "Expected value for 'delim' but found none." in output + assert "Expected value for 'scope' but found none." in output + assert "Expected value for 'subject' but found none." in output + assert "git commit --edit --file=.git/COMMIT_EDITMSG" in output + assert "edit the commit message and retry the commit" in output + + def test_subprocess_fail__missing_args(cmd): result = subprocess.call(cmd) From 26392d2686f2c0a46bcf3b36f24dcd33cfd73b8f Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 16 Oct 2024 22:14:08 +0000 Subject: [PATCH 05/10] refactor(format): extract regex construction and usage --- conventional_pre_commit/format.py | 48 +++++++++++++++++++++++++------ tests/test_format.py | 28 ++++++++++++++++++ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/conventional_pre_commit/format.py b/conventional_pre_commit/format.py index 8e1822d..dc283d3 100644 --- a/conventional_pre_commit/format.py +++ b/conventional_pre_commit/format.py @@ -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{r_types(types)})?" + scope_pattern = f"(?P{r_scope(optional_scope, scopes=scopes)})?" + delim_pattern = f"(?P{r_delim()})?" + subject_pattern = f"(?P{r_subject()})?" + body_pattern = f"(?P{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 diff --git a/tests/test_format.py b/tests/test_format.py index 6377086..e57d3d8 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -436,6 +436,34 @@ def test_strip_verbose_commit_ignored__strips_double_verbose_ignored(): assert result == expected +def test_conventional_regex(): + regex = format.conventional_regex() + + assert isinstance(regex, re.Pattern) + assert "type" in regex.groupindex + assert "scope" in regex.groupindex + assert "delim" in regex.groupindex + assert "subject" in regex.groupindex + assert "body" in regex.groupindex + assert "multi" in regex.groupindex + assert "sep" in regex.groupindex + + +def test_conventional_match(): + match = format.conventional_match( + """test(scope): subject line + +body copy +""" + ) + assert match + assert match.group("type") == "test" + assert match.group("scope") == "(scope)" + assert match.group("delim") == ":" + assert match.group("subject").strip() == "subject line" + assert match.group("body").strip() == "body copy" + + @pytest.mark.parametrize("type", format.DEFAULT_TYPES) def test_is_conventional__default_type(type): input = f"{type}: message" From 7c3a81452ed110c9da9b2d4722f2546f418868a0 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 16 Oct 2024 22:57:06 +0000 Subject: [PATCH 06/10] feat(output): verbose indicates specific errors --- conventional_pre_commit/output.py | 46 ++++++++++++++++++++++++++++--- tests/test_output.py | 41 +++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/conventional_pre_commit/output.py b/conventional_pre_commit/output.py index 0658805..3aa29ba 100644 --- a/conventional_pre_commit/output.py +++ b/conventional_pre_commit/output.py @@ -1,4 +1,7 @@ import os +from typing import List, Optional + +from conventional_pre_commit import format class Colors: @@ -25,15 +28,50 @@ def verbose_arg(): return os.linesep.join(lines) -def fail_verbose(commit_msg): +def fail_verbose(commit_msg: str, types=format.DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None): + match = format.conventional_match(commit_msg, types, optional_scope, scopes) lines = [ "", - f"{Colors.YELLOW}Run{Colors.RESTORE}", + f"{Colors.YELLOW}Conventional Commit messages follow a pattern like:", + "", + f"{Colors.RESTORE} type(scope): subject", "", - " git commit --edit --file=.git/COMMIT_EDITMSG", + " extended body", "", - f"{Colors.YELLOW}to edit the commit message and retry the commit.{Colors.RESTORE}", ] + + 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"{Colors.YELLOW}Please correct the following errors:{Colors.RESTORE}") + lines.append("") + for group in [g for g, v in groups.items() if not v]: + if group == "scope": + if scopes: + lines.append(f" - Expected value for 'scope' from: {','.join(scopes)}") + else: + lines.append(" - Expected value for 'scope' but found none.") + else: + lines.append(f" - Expected value for '{group}' but found none.") + + lines.extend( + [ + "", + f"{Colors.YELLOW}Run:{Colors.RESTORE}", + "", + " git commit --edit --file=.git/COMMIT_EDITMSG", + "", + f"{Colors.YELLOW}to edit the commit message and retry the commit.{Colors.RESTORE}", + ] + ) return os.linesep.join(lines) diff --git a/tests/test_output.py b/tests/test_output.py index fd8f267..479c7b5 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,3 +1,4 @@ +import os from conventional_pre_commit.output import fail, fail_verbose, unicode_decode_error @@ -11,12 +12,48 @@ def test_fail(): def test_fail_verbose(): - output = fail_verbose("commit msg") - + output = fail_verbose("commit msg", optional_scope=False) + + assert "Conventional Commit messages follow a pattern like" in output + assert f"type(scope): subject{os.linesep}{os.linesep} extended body" in output + assert "Expected value for 'type' but found none." in output + assert "Expected value for 'delim' but found none." in output + assert "Expected value for 'scope' but found none." in output + assert "Expected value for 'subject' but found none." in output assert "git commit --edit --file=.git/COMMIT_EDITMSG" in output assert "edit the commit message and retry the commit" in output +def test_fail_verbose__optional_scope(): + output = fail_verbose("commit msg", optional_scope=True) + + assert "Expected value for 'scope' but found none." not in output + + +def test_fail_verbose__missing_subject(): + output = fail_verbose("feat(scope):", optional_scope=False) + + assert "Expected value for 'subject' but found none." in output + assert "Expected value for 'type' but found none." not in output + assert "Expected value for 'scope' but found none." not in output + + +def test_fail_verbose_no_body_sep(): + output = fail_verbose( + """feat(scope): subject +body without blank line +""", + optional_scope=False, + ) + + assert "Expected value for 'sep' but found none." in output + assert "Expected value for 'multi' but found none." not in output + + assert "Expected value for 'subject' but found none." not in output + assert "Expected value for 'type' but found none." not in output + assert "Expected value for 'scope' but found none." not in output + + def test_unicode_decode_error(): output = unicode_decode_error() From f2c0be15ef8eca4ced8343c3d65ae213ea45b34e Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 16 Oct 2024 23:43:22 +0000 Subject: [PATCH 07/10] chore: make local config verbose --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eafc0ea..20599bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,3 +51,4 @@ repos: entry: conventional-pre-commit language: python stages: [commit-msg] + args: [--verbose] From 6d66cf116ff9689e08d1e49043e565492e6dd921 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 17 Oct 2024 01:22:38 +0000 Subject: [PATCH 08/10] feat(output): color is optional, on by default --- conventional_pre_commit/output.py | 64 ++++++++++++++------- tests/test_output.py | 93 +++++++++++++++++++++++++------ 2 files changed, 121 insertions(+), 36 deletions(-) diff --git a/conventional_pre_commit/output.py b/conventional_pre_commit/output.py index 3aa29ba..8d71385 100644 --- a/conventional_pre_commit/output.py +++ b/conventional_pre_commit/output.py @@ -10,31 +10,55 @@ class Colors: RESTORE = "\033[0m" YELLOW = "\033[00;33m" + def __init__(self, enabled=True): + self.enabled = enabled -def fail(commit_msg): + @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"{Colors.LRED}[Bad commit message] >>{Colors.RESTORE} {commit_msg}" - f"{Colors.YELLOW}Your commit message does not follow Conventional Commits formatting{Colors.RESTORE}", - f"{Colors.LBLUE}https://www.conventionalcommits.org/{Colors.RESTORE}", + 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(): +def verbose_arg(use_color=True): + c = Colors(use_color) lines = [ "", - f"{Colors.YELLOW}Use the {Colors.RESTORE}--verbose{Colors.YELLOW} arg for more information{Colors.RESTORE}", + 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): +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"{Colors.YELLOW}Conventional Commit messages follow a pattern like:", + f"{c.yellow}Conventional Commit messages follow a pattern like:", "", - f"{Colors.RESTORE} type(scope): subject", + f"{c.restore} type(scope): subject", "", " extended body", "", @@ -51,35 +75,37 @@ def fail_verbose(commit_msg: str, types=format.DEFAULT_TYPES, optional_scope=Tru groups.pop("sep", None) if groups.keys(): - lines.append(f"{Colors.YELLOW}Please correct the following errors:{Colors.RESTORE}") + 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: - lines.append(f" - Expected value for 'scope' from: {','.join(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(" - Expected value for 'scope' but found none.") + lines.append(f"{c.yellow} - Expected value for {c.restore}scope{c.yellow} but found none.{c.restore}") else: - lines.append(f" - Expected value for '{group}' but found none.") + lines.append(f"{c.yellow} - Expected value for {c.restore}{group}{c.yellow} but found none.{c.restore}") lines.extend( [ "", - f"{Colors.YELLOW}Run:{Colors.RESTORE}", + f"{c.yellow}Run:{c.restore}", "", " git commit --edit --file=.git/COMMIT_EDITMSG", "", - f"{Colors.YELLOW}to edit the commit message and retry the commit.{Colors.RESTORE}", + f"{c.yellow}to edit the commit message and retry the commit.{c.restore}", ] ) return os.linesep.join(lines) -def unicode_decode_error(): +def unicode_decode_error(use_color=True): + c = Colors(use_color) return f""" -{Colors.LRED}[Bad commit message encoding]{Colors.RESTORE} +{c.red}[Bad commit message encoding]{c.restore} -{Colors.YELLOW}conventional-pre-commit couldn't decode your commit message. +{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 {Colors.LBLUE}https://git-scm.com/docs/git-commit/#_discussion{Colors.YELLOW} for more.{Colors.RESTORE} +See {c.blue}https://git-scm.com/docs/git-commit/#_discussion{c.yellow} for more.{c.restore} """ diff --git a/tests/test_output.py b/tests/test_output.py index 479c7b5..2eef0fc 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,62 +1,121 @@ import os -from conventional_pre_commit.output import fail, fail_verbose, unicode_decode_error +from conventional_pre_commit.output import Colors, fail, fail_verbose, unicode_decode_error + + +def test_colors(): + colors = Colors() + + assert colors.blue == colors.LBLUE + assert colors.red == colors.LRED + assert colors.restore == colors.RESTORE + assert colors.yellow == colors.YELLOW + + colors = Colors(enabled=False) + + assert colors.blue == "" + assert colors.red == "" + assert colors.restore == "" + assert colors.yellow == "" def test_fail(): output = fail("commit msg") + assert Colors.LRED in output + assert Colors.YELLOW in output + assert Colors.LBLUE in output + assert Colors.RESTORE in output + assert "Bad commit message" in output assert "commit msg" in output assert "Conventional Commits formatting" in output assert "https://www.conventionalcommits.org/" in output +def test_fail__no_color(): + output = fail("commit msg", use_color=False) + + assert Colors.LRED not in output + assert Colors.YELLOW not in output + assert Colors.LBLUE not in output + assert Colors.RESTORE not in output + + def test_fail_verbose(): output = fail_verbose("commit msg", optional_scope=False) + assert Colors.YELLOW in output + assert Colors.RESTORE in output + + output = output.replace(Colors.YELLOW, Colors.RESTORE).replace(Colors.RESTORE, "") + assert "Conventional Commit messages follow a pattern like" in output assert f"type(scope): subject{os.linesep}{os.linesep} extended body" in output - assert "Expected value for 'type' but found none." in output - assert "Expected value for 'delim' but found none." in output - assert "Expected value for 'scope' but found none." in output - assert "Expected value for 'subject' but found none." in output + assert "Expected value for type but found none." in output + assert "Expected value for delim but found none." in output + assert "Expected value for scope but found none." in output + assert "Expected value for subject but found none." in output assert "git commit --edit --file=.git/COMMIT_EDITMSG" in output assert "edit the commit message and retry the commit" in output +def test_fail_verbose__no_color(): + output = fail_verbose("commit msg", use_color=False) + + assert Colors.LRED not in output + assert Colors.YELLOW not in output + assert Colors.LBLUE not in output + assert Colors.RESTORE not in output + + def test_fail_verbose__optional_scope(): - output = fail_verbose("commit msg", optional_scope=True) + output = fail_verbose("commit msg", optional_scope=True, use_color=False) - assert "Expected value for 'scope' but found none." not in output + assert "Expected value for scope but found none." not in output def test_fail_verbose__missing_subject(): - output = fail_verbose("feat(scope):", optional_scope=False) + output = fail_verbose("feat(scope):", optional_scope=False, use_color=False) - assert "Expected value for 'subject' but found none." in output - assert "Expected value for 'type' but found none." not in output - assert "Expected value for 'scope' but found none." not in output + assert "Expected value for subject but found none." in output + assert "Expected value for type but found none." not in output + assert "Expected value for scope but found none." not in output -def test_fail_verbose_no_body_sep(): +def test_fail_verbose__no_body_sep(): output = fail_verbose( """feat(scope): subject body without blank line """, optional_scope=False, + use_color=False, ) - assert "Expected value for 'sep' but found none." in output - assert "Expected value for 'multi' but found none." not in output + assert "Expected value for sep but found none." in output + assert "Expected value for multi but found none." not in output - assert "Expected value for 'subject' but found none." not in output - assert "Expected value for 'type' but found none." not in output - assert "Expected value for 'scope' but found none." not in output + assert "Expected value for subject but found none." not in output + assert "Expected value for type but found none." not in output + assert "Expected value for scope but found none." not in output def test_unicode_decode_error(): output = unicode_decode_error() + assert Colors.LRED in output + assert Colors.YELLOW in output + assert Colors.LBLUE in output + assert Colors.RESTORE in output + assert "Bad commit message encoding" in output assert "UTF-8 encoding is assumed" in output assert "https://git-scm.com/docs/git-commit/#_discussion" in output + + +def test_unicode_decode_error__no_color(): + output = unicode_decode_error(use_color=False) + + assert Colors.LRED not in output + assert Colors.YELLOW not in output + assert Colors.LBLUE not in output + assert Colors.RESTORE not in output From 367e74098332159fb3ce37cd5bb561f5a2dcc49f Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Thu, 17 Oct 2024 01:42:27 +0000 Subject: [PATCH 09/10] feat(hook): arg to turn off color in output --- conventional_pre_commit/hook.py | 13 +++++++++---- tests/test_hook.py | 23 +++++++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/conventional_pre_commit/hook.py b/conventional_pre_commit/hook.py index 2557031..5ffb308 100644 --- a/conventional_pre_commit/hook.py +++ b/conventional_pre_commit/hook.py @@ -13,6 +13,7 @@ def main(argv=[]): ) 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." ) @@ -47,7 +48,7 @@ def main(argv=[]): with open(args.input, encoding="utf-8") as f: commit_msg = f.read() except UnicodeDecodeError: - print(output.unicode_decode_error()) + print(output.unicode_decode_error(args.color)) return RESULT_FAIL if args.scopes: scopes = args.scopes.split(",") @@ -61,12 +62,16 @@ def main(argv=[]): if format.is_conventional(commit_msg, args.types, args.optional_scope, scopes): return RESULT_SUCCESS - print(output.fail(commit_msg)) + print(output.fail(commit_msg, use_color=args.color)) if not args.verbose: - print(output.verbose_arg()) + print(output.verbose_arg(use_color=args.color)) else: - print(output.fail_verbose(commit_msg, args.types, args.optional_scope, scopes)) + print( + output.fail_verbose( + commit_msg, types=args.types, optional_scope=args.optional_scope, scopes=scopes, use_color=args.color + ) + ) return RESULT_FAIL diff --git a/tests/test_hook.py b/tests/test_hook.py index a452578..f21aa9a 100644 --- a/tests/test_hook.py +++ b/tests/test_hook.py @@ -4,6 +4,7 @@ import pytest from conventional_pre_commit.hook import RESULT_FAIL, RESULT_SUCCESS, main +from conventional_pre_commit.output import Colors @pytest.fixture @@ -103,16 +104,30 @@ def test_main_fail__verbose(bad_commit_path, capsys): captured = capsys.readouterr() output = captured.out + assert Colors.LBLUE in output + assert Colors.LRED in output + assert Colors.RESTORE in output + assert Colors.YELLOW in output assert "Conventional Commit messages follow a pattern like" in output assert f"type(scope): subject{os.linesep}{os.linesep} extended body" in output - assert "Expected value for 'type' but found none." in output - assert "Expected value for 'delim' but found none." in output - assert "Expected value for 'scope' but found none." in output - assert "Expected value for 'subject' but found none." in output assert "git commit --edit --file=.git/COMMIT_EDITMSG" in output assert "edit the commit message and retry the commit" in output +def test_main_fail__no_color(bad_commit_path, capsys): + result = main(["--verbose", "--no-color", bad_commit_path]) + + assert result == RESULT_FAIL + + captured = capsys.readouterr() + output = captured.out + + assert Colors.LBLUE not in output + assert Colors.LRED not in output + assert Colors.RESTORE not in output + assert Colors.YELLOW not in output + + def test_subprocess_fail__missing_args(cmd): result = subprocess.call(cmd) From dd0ce285135ce0b4e20380f0afd19ffffff59602 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Wed, 16 Oct 2024 23:57:55 +0000 Subject: [PATCH 10/10] docs: update README with new args, output --- README.md | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 6e7f8c3..15d6d3d 100644 --- a/README.md +++ b/README.md @@ -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:: @@ -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. @@ -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: