From f30c24993ff636345dcba91bcc0bb49f2a1cb673 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sat, 26 Dec 2020 11:50:26 -0500 Subject: [PATCH 1/5] refactor: move expected COMMITS_TREE to global --- tests/test_changelog.py | 537 ++++++++++++++++++++-------------------- 1 file changed, 268 insertions(+), 269 deletions(-) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index b1dc1dc0e7..2721c34795 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -509,293 +509,292 @@ def test_get_commit_tag_is_None(gitcommits, tags): assert current_key is None -def test_generate_tree_from_commits(gitcommits, tags): - parser = defaults.commit_parser - changelog_pattern = defaults.bump_pattern - tree = changelog.generate_tree_from_commits( - gitcommits, tags, parser, changelog_pattern - ) - - assert tuple(tree) == ( - { - "version": "v1.2.0", - "date": "2019-04-19", - "changes": { - "feat": [ - { - "scope": None, - "breaking": None, - "message": "custom cz plugins now support bumping version", - } - ] - }, +COMMITS_TREE = ( + { + "version": "v1.2.0", + "date": "2019-04-19", + "changes": { + "feat": [ + { + "scope": None, + "breaking": None, + "message": "custom cz plugins now support bumping version", + } + ] }, - { - "version": "v1.1.1", - "date": "2019-04-18", - "changes": { - "refactor": [ - { - "scope": None, - "breaking": None, - "message": "changed stdout statements", - }, - { - "scope": "schema", - "breaking": None, - "message": "command logic removed from commitizen base", - }, - { - "scope": "info", - "breaking": None, - "message": "command logic removed from commitizen base", - }, - { - "scope": "example", - "breaking": None, - "message": "command logic removed from commitizen base", - }, - { - "scope": "commit", - "breaking": None, - "message": "moved most of the commit logic to the commit command", - }, - ], - "fix": [ - { - "scope": "bump", - "breaking": None, - "message": "commit message now fits better with semver", - }, - { - "scope": None, - "breaking": None, - "message": "conventional commit 'breaking change' in body instead of title", - }, - ], - }, + }, + { + "version": "v1.1.1", + "date": "2019-04-18", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "changed stdout statements", + }, + { + "scope": "schema", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "info", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "example", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "commit", + "breaking": None, + "message": "moved most of the commit logic to the commit command", + }, + ], + "fix": [ + { + "scope": "bump", + "breaking": None, + "message": "commit message now fits better with semver", + }, + { + "scope": None, + "breaking": None, + "message": "conventional commit 'breaking change' in body instead of title", + }, + ], }, - { - "version": "v1.1.0", - "date": "2019-04-14", - "changes": { - "feat": [ - { - "scope": None, - "breaking": None, - "message": "new working bump command", - }, - {"scope": None, "breaking": None, "message": "create version tag"}, - { - "scope": None, - "breaking": None, - "message": "update given files with new version", - }, - { - "scope": "config", - "breaking": None, - "message": "new set key, used to set version to cfg", - }, - { - "scope": None, - "breaking": None, - "message": "support for pyproject.toml", - }, - { - "scope": None, - "breaking": None, - "message": "first semantic version bump implementation", - }, - ], - "fix": [ - { - "scope": None, - "breaking": None, - "message": "removed all from commit", - }, - { - "scope": None, - "breaking": None, - "message": "fix config file not working", - }, - ], - "refactor": [ - { - "scope": None, - "breaking": None, - "message": "added commands folder, better integration with decli", - } - ], - }, + }, + { + "version": "v1.1.0", + "date": "2019-04-14", + "changes": { + "feat": [ + { + "scope": None, + "breaking": None, + "message": "new working bump command", + }, + {"scope": None, "breaking": None, "message": "create version tag"}, + { + "scope": None, + "breaking": None, + "message": "update given files with new version", + }, + { + "scope": "config", + "breaking": None, + "message": "new set key, used to set version to cfg", + }, + { + "scope": None, + "breaking": None, + "message": "support for pyproject.toml", + }, + { + "scope": None, + "breaking": None, + "message": "first semantic version bump implementation", + }, + ], + "fix": [ + { + "scope": None, + "breaking": None, + "message": "removed all from commit", + }, + { + "scope": None, + "breaking": None, + "message": "fix config file not working", + }, + ], + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "added commands folder, better integration with decli", + } + ], }, - { - "version": "v1.0.0", - "date": "2019-03-01", - "changes": { - "refactor": [ - { - "scope": None, - "breaking": None, - "message": "removed delegator, added decli and many tests", - } - ], - "BREAKING CHANGE": [ - {"scope": None, "breaking": None, "message": "API is stable"} - ], - }, + }, + { + "version": "v1.0.0", + "date": "2019-03-01", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "removed delegator, added decli and many tests", + } + ], + "BREAKING CHANGE": [ + {"scope": None, "breaking": None, "message": "API is stable"} + ], }, - {"version": "1.0.0b2", "date": "2019-01-18", "changes": {}}, - { - "version": "v1.0.0b1", - "date": "2019-01-17", - "changes": { - "feat": [ - { - "scope": None, - "breaking": None, - "message": "py3 only, tests and conventional commits 1.0", - } - ] - }, + }, + {"version": "1.0.0b2", "date": "2019-01-18", "changes": {}}, + { + "version": "v1.0.0b1", + "date": "2019-01-17", + "changes": { + "feat": [ + { + "scope": None, + "breaking": None, + "message": "py3 only, tests and conventional commits 1.0", + } + ] }, - { - "version": "v0.9.11", - "date": "2018-12-17", - "changes": { - "fix": [ - { - "scope": "config", - "breaking": None, - "message": "load config reads in order without failing if there is no commitizen section", - } - ] - }, + }, + { + "version": "v0.9.11", + "date": "2018-12-17", + "changes": { + "fix": [ + { + "scope": "config", + "breaking": None, + "message": "load config reads in order without failing if there is no commitizen section", + } + ] }, - { - "version": "v0.9.10", - "date": "2018-09-22", - "changes": { - "fix": [ - { - "scope": None, - "breaking": None, - "message": "parse scope (this is my punishment for not having tests)", - } - ] - }, + }, + { + "version": "v0.9.10", + "date": "2018-09-22", + "changes": { + "fix": [ + { + "scope": None, + "breaking": None, + "message": "parse scope (this is my punishment for not having tests)", + } + ] }, - { - "version": "v0.9.9", - "date": "2018-09-22", - "changes": { - "fix": [ - {"scope": None, "breaking": None, "message": "parse scope empty"} - ] - }, + }, + { + "version": "v0.9.9", + "date": "2018-09-22", + "changes": { + "fix": [{"scope": None, "breaking": None, "message": "parse scope empty"}] }, - { - "version": "v0.9.8", - "date": "2018-09-22", - "changes": { - "fix": [ - { - "scope": "scope", - "breaking": None, - "message": "parse correctly again", - } - ] - }, + }, + { + "version": "v0.9.8", + "date": "2018-09-22", + "changes": { + "fix": [ + { + "scope": "scope", + "breaking": None, + "message": "parse correctly again", + } + ] }, - { - "version": "v0.9.7", - "date": "2018-09-22", - "changes": { - "fix": [ - {"scope": "scope", "breaking": None, "message": "parse correctly"} - ] - }, + }, + { + "version": "v0.9.7", + "date": "2018-09-22", + "changes": { + "fix": [{"scope": "scope", "breaking": None, "message": "parse correctly"}] }, - { - "version": "v0.9.6", - "date": "2018-09-19", - "changes": { - "refactor": [ - { - "scope": "conventionalCommit", - "breaking": None, - "message": "moved filters to questions instead of message", - } - ], - "fix": [ - { - "scope": "manifest", - "breaking": None, - "message": "included missing files", - } - ], - }, + }, + { + "version": "v0.9.6", + "date": "2018-09-19", + "changes": { + "refactor": [ + { + "scope": "conventionalCommit", + "breaking": None, + "message": "moved filters to questions instead of message", + } + ], + "fix": [ + { + "scope": "manifest", + "breaking": None, + "message": "included missing files", + } + ], }, - { - "version": "v0.9.5", - "date": "2018-08-24", - "changes": { - "fix": [ - { - "scope": "config", - "breaking": None, - "message": "home path for python versions between 3.0 and 3.5", - } - ] - }, + }, + { + "version": "v0.9.5", + "date": "2018-08-24", + "changes": { + "fix": [ + { + "scope": "config", + "breaking": None, + "message": "home path for python versions between 3.0 and 3.5", + } + ] }, - { - "version": "v0.9.4", - "date": "2018-08-02", - "changes": { - "feat": [{"scope": "cli", "breaking": None, "message": "added version"}] - }, + }, + { + "version": "v0.9.4", + "date": "2018-08-02", + "changes": { + "feat": [{"scope": "cli", "breaking": None, "message": "added version"}] }, - { - "version": "v0.9.3", - "date": "2018-07-28", - "changes": { - "feat": [ - { - "scope": "committer", - "breaking": None, - "message": "conventional commit is a bit more intelligent now", - } - ] - }, + }, + { + "version": "v0.9.3", + "date": "2018-07-28", + "changes": { + "feat": [ + { + "scope": "committer", + "breaking": None, + "message": "conventional commit is a bit more intelligent now", + } + ] }, - { - "version": "v0.9.2", - "date": "2017-11-11", - "changes": { - "refactor": [ - { - "scope": None, - "breaking": None, - "message": "renamed conventional_changelog to conventional_commits, not backward compatible", - } - ] - }, + }, + { + "version": "v0.9.2", + "date": "2017-11-11", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "renamed conventional_changelog to conventional_commits, not backward compatible", + } + ] }, - { - "version": "v0.9.1", - "date": "2017-11-11", - "changes": { - "fix": [ - { - "scope": "setup.py", - "breaking": None, - "message": "future is now required for every python version", - } - ] - }, + }, + { + "version": "v0.9.1", + "date": "2017-11-11", + "changes": { + "fix": [ + { + "scope": "setup.py", + "breaking": None, + "message": "future is now required for every python version", + } + ] }, + }, +) + + +def test_generate_tree_from_commits(gitcommits, tags): + parser = defaults.commit_parser + changelog_pattern = defaults.bump_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern ) + assert tuple(tree) == COMMITS_TREE + def test_render_changelog(gitcommits, tags, changelog_content): parser = defaults.commit_parser From 849421d85ddc8a3b28ecff7c8afc6dec92102954 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sat, 26 Dec 2020 11:50:47 -0500 Subject: [PATCH 2/5] feat(#319): add optional change_type_order --- commitizen/changelog.py | 18 ++++++++++++++-- commitizen/commands/changelog.py | 5 +++++ commitizen/cz/base.py | 1 + commitizen/cz/customize/customize.py | 5 +++++ commitizen/defaults.py | 2 ++ docs/customization.md | 10 ++++++--- pyproject.toml | 4 ++++ tests/commands/test_changelog_command.py | 1 + tests/test_changelog.py | 27 ++++++++++++++++++++++++ tests/test_cz_customize.py | 14 ++++++++++++ 10 files changed, 82 insertions(+), 5 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 9084561c15..607c27ede3 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -24,9 +24,10 @@ - [x] hook after changelog is generated (api calls) - [x] add support for change_type maps """ + import os import re -from collections import defaultdict +from collections import OrderedDict, defaultdict from datetime import date from typing import Callable, Dict, Iterable, List, Optional @@ -98,7 +99,7 @@ def generate_tree_from_commits( "date": current_tag_date, "changes": changes, } - # TODO: Check if tag matches the version pattern, otherwie skip it. + # TODO: Check if tag matches the version pattern, otherwise skip it. # This in order to prevent tags that are not versions. current_tag_name = commit_tag.name current_tag_date = commit_tag.date @@ -128,6 +129,19 @@ def generate_tree_from_commits( yield {"version": current_tag_name, "date": current_tag_date, "changes": changes} +def order_changelog_tree(tree: Iterable, change_type_order: List[str]) -> Iterable: + sorted_tree = [] + for entry in tree: + entry_change_types = sorted(entry["changes"].keys()) + ordered_change_types = [] + for ct in change_type_order + entry_change_types: + if ct in entry_change_types and ct not in ordered_change_types: + ordered_change_types.append(ct) + changes = [(ct, entry["changes"][ct]) for ct in ordered_change_types] + sorted_tree.append({**entry, **{"changes": OrderedDict(changes)}}) + return sorted_tree + + def render_changelog(tree: Iterable) -> str: loader = PackageLoader("commitizen", "templates") env = Environment(loader=loader, trim_blocks=True) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 62d175d27d..535632dfc0 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -43,6 +43,9 @@ def __init__(self, config: BaseConfig, args): self.change_type_map = ( self.config.settings.get("change_type_map") or self.cz.change_type_map ) + self.change_type_order = ( + self.config.settings.get("change_type_order") or self.cz.change_type_order + ) def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str: """Try to find the 'start_rev'. @@ -109,6 +112,8 @@ def __call__(self): change_type_map=change_type_map, changelog_message_builder_hook=changelog_message_builder_hook, ) + if self.change_type_order: + tree = changelog.order_changelog_tree(tree, self.change_type_order) changelog_out = changelog.render_changelog(tree) changelog_out = changelog_out.lstrip("\n") diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index 5871879992..734852c868 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -29,6 +29,7 @@ class BaseCommitizen(metaclass=ABCMeta): commit_parser: Optional[str] = r"(?P.*)" changelog_pattern: Optional[str] = r".*" change_type_map: Optional[Dict[str, str]] = None + change_type_order: Optional[List[str]] = None # Executed per message parsed by the commitizen changelog_message_builder_hook: Optional[ diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py index 1c0bb98baf..acf205d06e 100644 --- a/commitizen/cz/customize/customize.py +++ b/commitizen/cz/customize/customize.py @@ -16,6 +16,7 @@ class CustomizeCommitsCz(BaseCommitizen): bump_pattern = defaults.bump_pattern bump_map = defaults.bump_map + change_type_order = defaults.change_type_order def __init__(self, config: BaseConfig): super(CustomizeCommitsCz, self).__init__(config) @@ -32,6 +33,10 @@ def __init__(self, config: BaseConfig): if custom_bump_map: self.bump_map = custom_bump_map + custom_change_type_order = self.custom_settings.get("change_type_order") + if custom_change_type_order: + self.change_type_order = custom_change_type_order + def questions(self) -> List[Dict[str, Any]]: return self.custom_settings.get("questions") diff --git a/commitizen/defaults.py b/commitizen/defaults.py index c5f07992f6..77fc4b0a50 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -40,5 +40,7 @@ ) bump_message = "bump: version $current_version → $new_version" +change_type_order = ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"] + commit_parser = r"^(?Pfeat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?:\s(?P.*)?" # noqa version_parser = r"(?P([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?)" diff --git a/docs/customization.md b/docs/customization.md index 0d86dc057f..bd15498051 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -23,6 +23,7 @@ schema = ": " schema_pattern = "(feature|bug fix):(\\s.*)" bump_pattern = "^(break|new|fix|hotfix)" bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"} +change_type_order = ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"] info_path = "cz_customize_info.txt" info = """ This is customized info @@ -51,7 +52,7 @@ The equivalent example for a json config file: ```json { "commitizen": { - "name": "cz_customize", + "name": "cz_customize", "customize": { "message_template": "{{change_type}}:{% if show_message %} {{message}}{% endif %}", "example": "feature: this feature enable customize through config file", @@ -64,7 +65,8 @@ The equivalent example for a json config file: "fix": "PATCH", "hotfix": "PATCH" }, - "info_path": "cz_customize_info.txt", + "change_type_order": ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"], + "info_path": "cz_customize_info.txt", "info": "This is customized info", "questions": [ { @@ -114,6 +116,7 @@ commitizen: new: MINOR fix: PATCH hotfix: PATCH + change_type_order: ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"] info_path: cz_customize_info.txt info: This is customized info questions: @@ -146,6 +149,7 @@ commitizen: | `info` | `str` | `None` | (OPTIONAL) Explanation of the commit rules. Used by `cz info`. | | `bump_map` | `dict` | `None` | (OPTIONAL) Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) | | `bump_pattern` | `str` | `None` | (OPTIONAL) Regex to extract information from commit (subject and body) | +| `change_type_order`| `str` | `None` | (OPTIONAL) List of strings used to order the Changelog. All other types will be sorted alphabetically. Default is `["BREAKING CHANGE", "feat", "fix", "refactor", "perf"]` | #### Detailed `questions` content @@ -298,7 +302,7 @@ You can customize it of course, and this are the variables you need to add to yo | Parameter | Type | Required | Description | | -------------------------------- | ------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `commit_parser` | `str` | NO | Regex which should provide the variables explained in the [changelog description][changelog-des] | -| `changelog_pattern` | `str` | NO | Regex to validate the commits, this is useful to skip commits that don't meet your rulling standards like a Merge. Usually the same as bump_pattern | +| `changelog_pattern` | `str` | NO | Regex to validate the commits, this is useful to skip commits that don't meet your ruling standards like a Merge. Usually the same as bump_pattern | | `change_type_map` | `dict` | NO | Convert the title of the change type that will appear in the changelog, if a value is not found, the original will be provided | | `changelog_message_builder_hook` | `method: (dict, git.GitCommit) -> dict` | NO | Customize with extra information your message output, like adding links, this function is executed per parsed commit. Each GitCommit contains the following attrs: `rev`, `title`, `body`, `author`, `author_email` | | `changelog_hook` | `method: (full_changelog: str, partial_changelog: Optional[str]) -> str` | NO | Receives the whole and partial (if used incremental) changelog. Useful to send slack messages or notify a compliance department. Must return the full_changelog | diff --git a/pyproject.toml b/pyproject.toml index de30c25821..0c65367360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,9 @@ freezegun = "^0.3.15" pydocstyle = "^5.0.2" pre-commit = "^2.6.0" +# FIXME: Remove for submission (testing issues/319-only) +pytest-watch = "*" + [tool.poetry.scripts] cz = "commitizen.cli:main" git-cz = "commitizen.cli:main" @@ -81,6 +84,7 @@ include_trailing_comma = true force_grid_wrap = 0 combine_as_imports = true line_length = 88 +known_first_party = ["commitizen", "tests"] [tool.coverage] [tool.coverage.report] diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index aceaaf9d96..e5993dbdfa 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -249,6 +249,7 @@ def test_changelog_hook(mocker, config): create_file_and_commit("refactor: is in changelog") create_file_and_commit("Merge into master") + config.settings["change_type_order"] = ["Refactor", "Feat"] changelog = Changelog( config, {"unreleased_version": None, "incremental": True, "dry_run": False} ) diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 2721c34795..440f407ac2 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -796,6 +796,33 @@ def test_generate_tree_from_commits(gitcommits, tags): assert tuple(tree) == COMMITS_TREE +@pytest.mark.parametrize( + "change_type_order, expected_reordering", + ( + ([], {}), + ( + ["BREAKING CHANGE", "refactor"], + { + 2: (["refactor", "feat", "fix"], ["feat", "fix", "refactor"]), + 3: (["BREAKING CHANGE", "refactor"], ["refactor", "BREAKING CHANGE"]), + }, + ), + ), +) +def test_order_changelog_tree(change_type_order, expected_reordering): + tree = tuple(changelog.order_changelog_tree(COMMITS_TREE, change_type_order)) + + index_of_reordered_entry = [*expected_reordering.keys()] + for index, entry in enumerate(tuple(tree)): + if index in index_of_reordered_entry: + sorted_order, original_order = expected_reordering[index] + assert [*tree[index].keys()] == [*COMMITS_TREE[index].keys()] + assert [*tree[index]["changes"].keys()] == sorted_order + assert [*COMMITS_TREE[index]["changes"].keys()] == original_order + else: + assert [*entry["changes"].keys()] == [*tree[index]["changes"].keys()] + + def test_render_changelog(gitcommits, tags, changelog_content): parser = defaults.commit_parser changelog_pattern = defaults.bump_pattern diff --git a/tests/test_cz_customize.py b/tests/test_cz_customize.py index 479fcef7c4..e919093e04 100644 --- a/tests/test_cz_customize.py +++ b/tests/test_cz_customize.py @@ -13,6 +13,7 @@ bump_pattern = "^(break|new|fix|hotfix)" bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"} + change_type_order = ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"] info = "This is a customized cz." [[tool.commitizen.customize.questions]] @@ -56,6 +57,7 @@ "fix": "PATCH", "hotfix": "PATCH" }, + "change_type_order": ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"], "info": "This is a customized cz.", "questions": [ { @@ -107,6 +109,7 @@ new: MINOR fix: PATCH hotfix: PATCH + change_type_order: ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"] info: This is a customized cz. questions: - type: list @@ -274,6 +277,17 @@ def test_bump_map(config): } +def test_change_type_order(config): + cz = CustomizeCommitsCz(config) + assert cz.change_type_order == [ + "perf", + "BREAKING CHANGE", + "feat", + "fix", + "refactor", + ] + + def test_questions(config): cz = CustomizeCommitsCz(config) questions = cz.questions() From 80c905f6c602ad369e88c6f464b6a003f07ccc3e Mon Sep 17 00:00:00 2001 From: Kyle King Date: Wed, 30 Dec 2020 06:43:20 -0500 Subject: [PATCH 3/5] refactor(#323): address PR feedback --- commitizen/changelog.py | 19 +++++++++++++------ docs/customization.md | 4 ++-- pyproject.toml | 3 --- tests/test_changelog.py | 32 ++++++++++++++++++++++++-------- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 607c27ede3..fc2680544d 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -130,14 +130,21 @@ def generate_tree_from_commits( def order_changelog_tree(tree: Iterable, change_type_order: List[str]) -> Iterable: + if len(set(change_type_order)) != len(change_type_order): + raise RuntimeError( + f"Change types contain duplicates types ({change_type_order})" + ) + sorted_tree = [] for entry in tree: - entry_change_types = sorted(entry["changes"].keys()) - ordered_change_types = [] - for ct in change_type_order + entry_change_types: - if ct in entry_change_types and ct not in ordered_change_types: - ordered_change_types.append(ct) - changes = [(ct, entry["changes"][ct]) for ct in ordered_change_types] + ordered_change_types = change_type_order + sorted( + set(entry["changes"].keys()) - set(change_type_order) + ) + changes = [ + (ct, entry["changes"][ct]) + for ct in ordered_change_types + if ct in entry["changes"] + ] sorted_tree.append({**entry, **{"changes": OrderedDict(changes)}}) return sorted_tree diff --git a/docs/customization.md b/docs/customization.md index bd15498051..5e7caaaaaf 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -52,7 +52,7 @@ The equivalent example for a json config file: ```json { "commitizen": { - "name": "cz_customize", + "name": "cz_customize", "customize": { "message_template": "{{change_type}}:{% if show_message %} {{message}}{% endif %}", "example": "feature: this feature enable customize through config file", @@ -66,7 +66,7 @@ The equivalent example for a json config file: "hotfix": "PATCH" }, "change_type_order": ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"], - "info_path": "cz_customize_info.txt", + "info_path": "cz_customize_info.txt", "info": "This is customized info", "questions": [ { diff --git a/pyproject.toml b/pyproject.toml index 0c65367360..84e931804a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,9 +71,6 @@ freezegun = "^0.3.15" pydocstyle = "^5.0.2" pre-commit = "^2.6.0" -# FIXME: Remove for submission (testing issues/319-only) -pytest-watch = "*" - [tool.poetry.scripts] cz = "commitizen.cli:main" git-cz = "commitizen.cli:main" diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 440f407ac2..6696a14348 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -803,26 +803,42 @@ def test_generate_tree_from_commits(gitcommits, tags): ( ["BREAKING CHANGE", "refactor"], { - 2: (["refactor", "feat", "fix"], ["feat", "fix", "refactor"]), - 3: (["BREAKING CHANGE", "refactor"], ["refactor", "BREAKING CHANGE"]), + "1.1.0": { + "original": ["feat", "fix", "refactor"], + "sorted": ["refactor", "feat", "fix"], + }, + "1.0.0": { + "original": ["refactor", "BREAKING CHANGE"], + "sorted": ["BREAKING CHANGE", "refactor"], + }, }, ), ), ) def test_order_changelog_tree(change_type_order, expected_reordering): - tree = tuple(changelog.order_changelog_tree(COMMITS_TREE, change_type_order)) + tree = changelog.order_changelog_tree(COMMITS_TREE, change_type_order) - index_of_reordered_entry = [*expected_reordering.keys()] for index, entry in enumerate(tuple(tree)): - if index in index_of_reordered_entry: - sorted_order, original_order = expected_reordering[index] + version = tree[index]["version"] + if version in expected_reordering: + # Verify that all keys are present assert [*tree[index].keys()] == [*COMMITS_TREE[index].keys()] - assert [*tree[index]["changes"].keys()] == sorted_order - assert [*COMMITS_TREE[index]["changes"].keys()] == original_order + # Verify that the reorder only impacted the returned dict and not the original + expected = expected_reordering[version] + assert [*tree[index]["changes"].keys()] == expected["sorted"] + assert [*COMMITS_TREE[index]["changes"].keys()] == expected["original"] else: assert [*entry["changes"].keys()] == [*tree[index]["changes"].keys()] +def test_order_changelog_tree_raises(): + change_type_order = ["BREAKING CHANGE", "feat", "refactor", "feat"] + with pytest.raises(RuntimeError) as excinfo: + changelog.order_changelog_tree(COMMITS_TREE, change_type_order) + + assert " duplicate" in str(excinfo) + + def test_render_changelog(gitcommits, tags, changelog_content): parser = defaults.commit_parser changelog_pattern = defaults.bump_pattern From f7392ced59462cce080fff8c88c71a804fba9796 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Wed, 30 Dec 2020 06:45:08 -0500 Subject: [PATCH 4/5] style: restore whitespace for info_path in JSON --- docs/customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/customization.md b/docs/customization.md index 5e7caaaaaf..204395ed4b 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -66,7 +66,7 @@ The equivalent example for a json config file: "hotfix": "PATCH" }, "change_type_order": ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"], - "info_path": "cz_customize_info.txt", + "info_path": "cz_customize_info.txt", "info": "This is customized info", "questions": [ { From 0e3482a6feb80731e5ab32ba6e4e8d26979a031f Mon Sep 17 00:00:00 2001 From: Kyle King Date: Thu, 31 Dec 2020 08:29:43 -0500 Subject: [PATCH 5/5] refactor: raise an InvalidConfigurationError --- commitizen/changelog.py | 3 ++- commitizen/exceptions.py | 5 +++++ docs/exit_codes.md | 1 + tests/test_changelog.py | 5 +++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index fc2680544d..ca36015ed9 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -34,6 +34,7 @@ from jinja2 import Environment, PackageLoader from commitizen import defaults +from commitizen.exceptions import InvalidConfigurationError from commitizen.git import GitCommit, GitTag CATEGORIES = [ @@ -131,7 +132,7 @@ def generate_tree_from_commits( def order_changelog_tree(tree: Iterable, change_type_order: List[str]) -> Iterable: if len(set(change_type_order)) != len(change_type_order): - raise RuntimeError( + raise InvalidConfigurationError( f"Change types contain duplicates types ({change_type_order})" ) diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index 84219f7fd1..8293688715 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -23,6 +23,7 @@ class ExitCode(enum.IntEnum): NO_REVISION = 16 CURRENT_VERSION_NOT_FOUND = 17 INVALID_COMMAND_ARGUMENT = 18 + INVALID_CONFIGURATION = 19 class CommitizenException(Exception): @@ -137,3 +138,7 @@ class NoCommandFoundError(CommitizenException): class InvalidCommandArgumentError(CommitizenException): exit_code = ExitCode.INVALID_COMMAND_ARGUMENT + + +class InvalidConfigurationError(CommitizenException): + exit_code = ExitCode.INVALID_CONFIGURATION diff --git a/docs/exit_codes.md b/docs/exit_codes.md index d83c93d991..cae66b8bab 100644 --- a/docs/exit_codes.md +++ b/docs/exit_codes.md @@ -26,3 +26,4 @@ These exit codes can be found in `commitizen/exceptions.py::ExitCode`. | NoRevisionError | 16 | No revision found | | CurrentVersionNotFoundError | 17 | current version cannot be found in *version_files* | | InvalidCommandArgumentError | 18 | The argument provide to command is invalid (e.g. `cz check -commit-msg-file filename --rev-range master..`) | +| InvalidConfigurationError | 19 | An error was found in the Commitizen Configuration, such as duplicates in `change_type_order` | diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 6696a14348..055e4fc916 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,6 +1,7 @@ import pytest from commitizen import changelog, defaults, git +from commitizen.exceptions import InvalidConfigurationError COMMITS_DATA = [ { @@ -833,10 +834,10 @@ def test_order_changelog_tree(change_type_order, expected_reordering): def test_order_changelog_tree_raises(): change_type_order = ["BREAKING CHANGE", "feat", "refactor", "feat"] - with pytest.raises(RuntimeError) as excinfo: + with pytest.raises(InvalidConfigurationError) as excinfo: changelog.order_changelog_tree(COMMITS_TREE, change_type_order) - assert " duplicate" in str(excinfo) + assert "Change types contain duplicates types" in str(excinfo) def test_render_changelog(gitcommits, tags, changelog_content):