From 8f1b0a56a073794db2f0b91304c5d15881b36403 Mon Sep 17 00:00:00 2001 From: Matt Speck <20689127+mjspeck@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:37:53 -0700 Subject: [PATCH 1/6] feat(format): added scopes argument to specify valid scopes Closes: #101 --- conventional_pre_commit/format.py | 19 ++++++++++++++++--- conventional_pre_commit/hook.py | 3 ++- tests/test_format.py | 12 ++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/conventional_pre_commit/format.py b/conventional_pre_commit/format.py index e79306f..94a76b7 100644 --- a/conventional_pre_commit/format.py +++ b/conventional_pre_commit/format.py @@ -1,4 +1,5 @@ import re +from typing import List, Optional CONVENTIONAL_TYPES = ["feat", "fix"] DEFAULT_TYPES = [ @@ -26,11 +27,23 @@ def r_types(types): return "|".join(types) -def r_scope(optional=True): +def r_scope(optional=True, scopes: Optional[List[str]] = None): """Regex str for an optional (scope).""" + if optional: + if scopes: + scopes_str = r_types(scopes) + # delims_str = r_types([":", ",", "-"]) + escaped_delimiters = list(map(re.escape, [":", ","])) # type: ignore + delimiters_pattern = r_types(escaped_delimiters) + return rf"\(\s*(?:{scopes_str})(?:\s*(?:{delimiters_pattern})\s*(?:{scopes_str}))*\s*\)" return r"(\([\w \/:,-]+\))?" else: + if scopes: + scopes_str = r_types(scopes) + escaped_delimiters = list(map(re.escape, [":", ","])) # type: ignore + delimiters_pattern = r_types(escaped_delimiters) + return rf"\(\s*(?:{scopes_str})(?:\s*(?:{delimiters_pattern})\s*(?:{scopes_str}))*\s*\)" return r"(\([\w \/:,-]+\))" @@ -79,7 +92,7 @@ def conventional_types(types=[]): return types -def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True): +def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[list[str]] = None): """ Returns True if input matches Conventional Commits formatting https://www.conventionalcommits.org @@ -89,7 +102,7 @@ def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True): input = strip_verbose_diff(input) input = strip_comments(input) types = conventional_types(types) - pattern = f"^({r_types(types)}){r_scope(optional_scope)}{r_delim()}{r_subject()}{r_body()}" + 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) diff --git a/conventional_pre_commit/hook.py b/conventional_pre_commit/hook.py index b0d0124..ebce41d 100644 --- a/conventional_pre_commit/hook.py +++ b/conventional_pre_commit/hook.py @@ -28,6 +28,7 @@ def main(argv=[]): action="store_true", help="Force commit to strictly follow Conventional Commits formatting. Disallows fixup! style commits.", ) + parser.add_argument("scopes", type=str, nargs="*", default=None, help="Optional list of scopes to support") if len(argv) < 1: argv = sys.argv[1:] @@ -56,7 +57,7 @@ def main(argv=[]): if format.has_autosquash_prefix(message): return RESULT_SUCCESS - if format.is_conventional(message, args.types, args.optional_scope): + if format.is_conventional(message, args.types, args.optional_scope, args.scopes): return RESULT_SUCCESS else: print( diff --git a/tests/test_format.py b/tests/test_format.py index 6b9507b..6a59ea2 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -64,6 +64,18 @@ def test_r_scope__special_chars(): assert regex.match("(some,thing)") +def test_r_scope__scopes(): + scopes_input = ["api", "client"] + result = format.r_scope(scopes=scopes_input) + regex = re.compile(result) + assert regex.match("(api)") + assert regex.match("(client)") + assert regex.match("(api, client)") + assert regex.match("(api: client)") + assert not regex.match("(test)") + assert not regex.match("(api; client)") + + def test_r_delim(): result = format.r_delim() regex = re.compile(result) From 71bfc2a5bd0fcd26c7fcf09aa364e33832b157f4 Mon Sep 17 00:00:00 2001 From: Matt Speck <20689127+mjspeck@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:21:13 -0700 Subject: [PATCH 2/6] refactor: cleaned up duplicate code in r_scope Refs: #101 --- conventional_pre_commit/format.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/conventional_pre_commit/format.py b/conventional_pre_commit/format.py index 94a76b7..3567b2e 100644 --- a/conventional_pre_commit/format.py +++ b/conventional_pre_commit/format.py @@ -27,23 +27,23 @@ def r_types(types): return "|".join(types) +def _get_scope_pattern(scopes: Optional[List[str]] = None): + scopes_str = r_types(scopes) + escaped_delimiters = list(map(re.escape, [":", ","])) # type: ignore + delimiters_pattern = r_types(escaped_delimiters) + return rf"\(\s*(?:{scopes_str})(?:\s*(?:{delimiters_pattern})\s*(?:{scopes_str}))*\s*\)" + + def r_scope(optional=True, scopes: Optional[List[str]] = None): """Regex str for an optional (scope).""" + if scopes: + scopes_pattern = _get_scope_pattern(scopes) + return scopes_pattern + if optional: - if scopes: - scopes_str = r_types(scopes) - # delims_str = r_types([":", ",", "-"]) - escaped_delimiters = list(map(re.escape, [":", ","])) # type: ignore - delimiters_pattern = r_types(escaped_delimiters) - return rf"\(\s*(?:{scopes_str})(?:\s*(?:{delimiters_pattern})\s*(?:{scopes_str}))*\s*\)" return r"(\([\w \/:,-]+\))?" else: - if scopes: - scopes_str = r_types(scopes) - escaped_delimiters = list(map(re.escape, [":", ","])) # type: ignore - delimiters_pattern = r_types(escaped_delimiters) - return rf"\(\s*(?:{scopes_str})(?:\s*(?:{delimiters_pattern})\s*(?:{scopes_str}))*\s*\)" return r"(\([\w \/:,-]+\))" From f51f6621b3d4d0ca1cca5d226ed8d57bc2c032de Mon Sep 17 00:00:00 2001 From: Matt Speck <20689127+mjspeck@users.noreply.github.com> Date: Sat, 6 Jul 2024 21:16:49 -0700 Subject: [PATCH 3/6] chore: added additional delimeters for scope Refs: #101, #102 --- conventional_pre_commit/format.py | 4 ++-- tests/test_format.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/conventional_pre_commit/format.py b/conventional_pre_commit/format.py index 3567b2e..672cb34 100644 --- a/conventional_pre_commit/format.py +++ b/conventional_pre_commit/format.py @@ -29,7 +29,7 @@ def r_types(types): def _get_scope_pattern(scopes: Optional[List[str]] = None): scopes_str = r_types(scopes) - escaped_delimiters = list(map(re.escape, [":", ","])) # type: ignore + escaped_delimiters = list(map(re.escape, [":", ",", "-", "/"])) # type: ignore delimiters_pattern = r_types(escaped_delimiters) return rf"\(\s*(?:{scopes_str})(?:\s*(?:{delimiters_pattern})\s*(?:{scopes_str}))*\s*\)" @@ -92,7 +92,7 @@ def conventional_types(types=[]): return types -def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[list[str]] = None): +def is_conventional(input, types=DEFAULT_TYPES, optional_scope=True, scopes: Optional[List[str]] = None): """ Returns True if input matches Conventional Commits formatting https://www.conventionalcommits.org diff --git a/tests/test_format.py b/tests/test_format.py index 6a59ea2..6940a21 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -72,6 +72,8 @@ def test_r_scope__scopes(): assert regex.match("(client)") assert regex.match("(api, client)") assert regex.match("(api: client)") + assert regex.match("(api/client)") + assert regex.match("(api-client)") assert not regex.match("(test)") assert not regex.match("(api; client)") From 22d377a66f31d847b174c1810964a42b9fc84b2d Mon Sep 17 00:00:00 2001 From: Matt Speck <20689127+mjspeck@users.noreply.github.com> Date: Sat, 6 Jul 2024 21:46:25 -0700 Subject: [PATCH 4/6] fix: fixed scopes option by making it a flag Refs: #101, #102 --- conventional_pre_commit/hook.py | 13 +++++++++++-- tests/conftest.py | 5 +++++ .../conventional_commit_with_multiple_scopes | 1 + tests/test_hook.py | 10 ++++++++++ 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/messages/conventional_commit_with_multiple_scopes diff --git a/conventional_pre_commit/hook.py b/conventional_pre_commit/hook.py index ebce41d..63c5f51 100644 --- a/conventional_pre_commit/hook.py +++ b/conventional_pre_commit/hook.py @@ -28,7 +28,12 @@ def main(argv=[]): action="store_true", help="Force commit to strictly follow Conventional Commits formatting. Disallows fixup! style commits.", ) - parser.add_argument("scopes", type=str, nargs="*", default=None, help="Optional list of scopes to support") + parser.add_argument( + "--scopes", + type=str, + default=None, + help="Optional list of scopes to support. Scopes should be separated by commas with no spaces (e.g. api,client)", + ) if len(argv) < 1: argv = sys.argv[1:] @@ -52,12 +57,16 @@ def main(argv=[]): """ ) return RESULT_FAIL + if args.scopes: + scopes = args.scopes.split(",") + else: + scopes = args.scopes if not args.strict: if format.has_autosquash_prefix(message): return RESULT_SUCCESS - if format.is_conventional(message, args.types, args.optional_scope, args.scopes): + if format.is_conventional(message, args.types, args.optional_scope, scopes): return RESULT_SUCCESS else: print( diff --git a/tests/conftest.py b/tests/conftest.py index b056cb7..83034ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,3 +52,8 @@ def conventional_commit_bad_multi_line_path(): @pytest.fixture def conventional_commit_multi_line_path(): return get_message_path("conventional_commit_multi_line") + + +@pytest.fixture +def conventional_commit_with_multiple_scopes_path(): + return get_message_path("conventional_commit_with_multiple_scopes") diff --git a/tests/messages/conventional_commit_with_multiple_scopes b/tests/messages/conventional_commit_with_multiple_scopes new file mode 100644 index 0000000..77fec69 --- /dev/null +++ b/tests/messages/conventional_commit_with_multiple_scopes @@ -0,0 +1 @@ +feat(api,client): added new endpoint with client support diff --git a/tests/test_hook.py b/tests/test_hook.py index d842e93..f2d1ce6 100644 --- a/tests/test_hook.py +++ b/tests/test_hook.py @@ -142,6 +142,16 @@ def test_subprocess_success__conventional_with_scope(cmd, conventional_commit_wi assert result == RESULT_SUCCESS +def test_subprocess_success__conventional_with_multiple_scopes(cmd, conventional_commit_with_multiple_scopes_path): + result = subprocess.call((cmd, "--scopes", "api,client", conventional_commit_with_multiple_scopes_path)) + assert result == RESULT_SUCCESS + + +def test_subprocess_fail__conventional_with_multiple_scopes(cmd, conventional_commit_with_multiple_scopes_path): + result = subprocess.call((cmd, "--scopes", "api", conventional_commit_with_multiple_scopes_path)) + assert result == RESULT_FAIL + + def test_subprocess_success__fixup_commit(cmd, fixup_commit_path): result = subprocess.call((cmd, fixup_commit_path)) From 95b5e456b5b8944d81ff5cbaab238debf4548bd1 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Fri, 12 Jul 2024 16:54:09 +0000 Subject: [PATCH 5/6] chore: reorder scopes arg so --force-scope and --scopes list next to each other --- conventional_pre_commit/hook.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conventional_pre_commit/hook.py b/conventional_pre_commit/hook.py index 63c5f51..25d6090 100644 --- a/conventional_pre_commit/hook.py +++ b/conventional_pre_commit/hook.py @@ -23,17 +23,17 @@ def main(argv=[]): parser.add_argument( "--force-scope", action="store_false", default=True, dest="optional_scope", help="Force commit to have scope defined." ) - parser.add_argument( - "--strict", - action="store_true", - help="Force commit to strictly follow Conventional Commits formatting. Disallows fixup! style commits.", - ) parser.add_argument( "--scopes", type=str, default=None, help="Optional list of scopes to support. Scopes should be separated by commas with no spaces (e.g. api,client)", ) + parser.add_argument( + "--strict", + action="store_true", + help="Force commit to strictly follow Conventional Commits formatting. Disallows fixup! style commits.", + ) if len(argv) < 1: argv = sys.argv[1:] From 3b6ff059cede2c7f564265fa380614335dcce531 Mon Sep 17 00:00:00 2001 From: Kegan Maher Date: Fri, 12 Jul 2024 16:54:54 +0000 Subject: [PATCH 6/6] docs: update with new --scope flag --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ccb30f4..6e7f8c3 100644 --- a/README.md +++ b/README.md @@ -129,18 +129,19 @@ print(is_conventional("custom: this is a conventional commit", types=["custom"]) ```shell $ conventional-pre-commit -h -usage: conventional-pre-commit [-h] [--force-scope] [--strict] [types ...] input +usage: conventional-pre-commit [-h] [--force-scope] [--scopes SCOPES] [--strict] [types ...] input Check a git commit message for Conventional Commits formatting. positional arguments: - types Optional list of types to support - input A file containing a git commit message + types Optional list of types to support + input A file containing a git commit message options: - -h, --help show this help message and exit - --force-scope Force commit to have scope defined. - --strict Force commit to strictly follow Conventional Commits formatting. Disallows fixup! style commits. + -h, --help show this help message and exit + --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. ``` Supply arguments on the command-line, or via the pre-commit `hooks.args` property: