Skip to content

Commit fb08300

Browse files
KyleKingLee-W
authored andcommitted
feat(#319): add optional change_type_order
1 parent 1474d39 commit fb08300

File tree

10 files changed

+82
-5
lines changed

10 files changed

+82
-5
lines changed

commitizen/changelog.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@
2424
- [x] hook after changelog is generated (api calls)
2525
- [x] add support for change_type maps
2626
"""
27+
2728
import os
2829
import re
29-
from collections import defaultdict
30+
from collections import OrderedDict, defaultdict
3031
from datetime import date
3132
from typing import Callable, Dict, Iterable, List, Optional
3233

@@ -98,7 +99,7 @@ def generate_tree_from_commits(
9899
"date": current_tag_date,
99100
"changes": changes,
100101
}
101-
# TODO: Check if tag matches the version pattern, otherwie skip it.
102+
# TODO: Check if tag matches the version pattern, otherwise skip it.
102103
# This in order to prevent tags that are not versions.
103104
current_tag_name = commit_tag.name
104105
current_tag_date = commit_tag.date
@@ -128,6 +129,19 @@ def generate_tree_from_commits(
128129
yield {"version": current_tag_name, "date": current_tag_date, "changes": changes}
129130

130131

132+
def order_changelog_tree(tree: Iterable, change_type_order: List[str]) -> Iterable:
133+
sorted_tree = []
134+
for entry in tree:
135+
entry_change_types = sorted(entry["changes"].keys())
136+
ordered_change_types = []
137+
for ct in change_type_order + entry_change_types:
138+
if ct in entry_change_types and ct not in ordered_change_types:
139+
ordered_change_types.append(ct)
140+
changes = [(ct, entry["changes"][ct]) for ct in ordered_change_types]
141+
sorted_tree.append({**entry, **{"changes": OrderedDict(changes)}})
142+
return sorted_tree
143+
144+
131145
def render_changelog(tree: Iterable) -> str:
132146
loader = PackageLoader("commitizen", "templates")
133147
env = Environment(loader=loader, trim_blocks=True)

commitizen/commands/changelog.py

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def __init__(self, config: BaseConfig, args):
4343
self.change_type_map = (
4444
self.config.settings.get("change_type_map") or self.cz.change_type_map
4545
)
46+
self.change_type_order = (
47+
self.config.settings.get("change_type_order") or self.cz.change_type_order
48+
)
4649

4750
def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str:
4851
"""Try to find the 'start_rev'.
@@ -109,6 +112,8 @@ def __call__(self):
109112
change_type_map=change_type_map,
110113
changelog_message_builder_hook=changelog_message_builder_hook,
111114
)
115+
if self.change_type_order:
116+
tree = changelog.order_changelog_tree(tree, self.change_type_order)
112117
changelog_out = changelog.render_changelog(tree)
113118
changelog_out = changelog_out.lstrip("\n")
114119

commitizen/cz/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class BaseCommitizen(metaclass=ABCMeta):
2929
commit_parser: Optional[str] = r"(?P<message>.*)"
3030
changelog_pattern: Optional[str] = r".*"
3131
change_type_map: Optional[Dict[str, str]] = None
32+
change_type_order: Optional[List[str]] = None
3233

3334
# Executed per message parsed by the commitizen
3435
changelog_message_builder_hook: Optional[

commitizen/cz/customize/customize.py

+5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
class CustomizeCommitsCz(BaseCommitizen):
1717
bump_pattern = defaults.bump_pattern
1818
bump_map = defaults.bump_map
19+
change_type_order = defaults.change_type_order
1920

2021
def __init__(self, config: BaseConfig):
2122
super(CustomizeCommitsCz, self).__init__(config)
@@ -32,6 +33,10 @@ def __init__(self, config: BaseConfig):
3233
if custom_bump_map:
3334
self.bump_map = custom_bump_map
3435

36+
custom_change_type_order = self.custom_settings.get("change_type_order")
37+
if custom_change_type_order:
38+
self.change_type_order = custom_change_type_order
39+
3540
def questions(self) -> List[Dict[str, Any]]:
3641
return self.custom_settings.get("questions")
3742

commitizen/defaults.py

+2
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,7 @@
4040
)
4141
bump_message = "bump: version $current_version → $new_version"
4242

43+
change_type_order = ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"]
44+
4345
commit_parser = r"^(?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?:\s(?P<message>.*)?" # noqa
4446
version_parser = r"(?P<version>([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?)"

docs/customization.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ schema = "<type>: <body>"
2323
schema_pattern = "(feature|bug fix):(\\s.*)"
2424
bump_pattern = "^(break|new|fix|hotfix)"
2525
bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"}
26+
change_type_order = ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"]
2627
info_path = "cz_customize_info.txt"
2728
info = """
2829
This is customized info
@@ -51,7 +52,7 @@ The equivalent example for a json config file:
5152
```json
5253
{
5354
"commitizen": {
54-
"name": "cz_customize",
55+
"name": "cz_customize",
5556
"customize": {
5657
"message_template": "{{change_type}}:{% if show_message %} {{message}}{% endif %}",
5758
"example": "feature: this feature enable customize through config file",
@@ -64,7 +65,8 @@ The equivalent example for a json config file:
6465
"fix": "PATCH",
6566
"hotfix": "PATCH"
6667
},
67-
"info_path": "cz_customize_info.txt",
68+
"change_type_order": ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"],
69+
"info_path": "cz_customize_info.txt",
6870
"info": "This is customized info",
6971
"questions": [
7072
{
@@ -114,6 +116,7 @@ commitizen:
114116
new: MINOR
115117
fix: PATCH
116118
hotfix: PATCH
119+
change_type_order: ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"]
117120
info_path: cz_customize_info.txt
118121
info: This is customized info
119122
questions:
@@ -146,6 +149,7 @@ commitizen:
146149
| `info` | `str` | `None` | (OPTIONAL) Explanation of the commit rules. Used by `cz info`. |
147150
| `bump_map` | `dict` | `None` | (OPTIONAL) Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) |
148151
| `bump_pattern` | `str` | `None` | (OPTIONAL) Regex to extract information from commit (subject and body) |
152+
| `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"]` |
149153

150154
#### Detailed `questions` content
151155

@@ -298,7 +302,7 @@ You can customize it of course, and this are the variables you need to add to yo
298302
| Parameter | Type | Required | Description |
299303
| -------------------------------- | ------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
300304
| `commit_parser` | `str` | NO | Regex which should provide the variables explained in the [changelog description][changelog-des] |
301-
| `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 |
305+
| `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 |
302306
| `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 |
303307
| `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` |
304308
| `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 |

pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ freezegun = "^0.3.15"
7171
pydocstyle = "^5.0.2"
7272
pre-commit = "^2.6.0"
7373

74+
# FIXME: Remove for submission (testing issues/319-only)
75+
pytest-watch = "*"
76+
7477
[tool.poetry.scripts]
7578
cz = "commitizen.cli:main"
7679
git-cz = "commitizen.cli:main"
@@ -81,6 +84,7 @@ include_trailing_comma = true
8184
force_grid_wrap = 0
8285
combine_as_imports = true
8386
line_length = 88
87+
known_first_party = ["commitizen", "tests"]
8488

8589
[tool.coverage]
8690
[tool.coverage.report]

tests/commands/test_changelog_command.py

+1
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ def test_changelog_hook(mocker, config):
249249
create_file_and_commit("refactor: is in changelog")
250250
create_file_and_commit("Merge into master")
251251

252+
config.settings["change_type_order"] = ["Refactor", "Feat"]
252253
changelog = Changelog(
253254
config, {"unreleased_version": None, "incremental": True, "dry_run": False}
254255
)

tests/test_changelog.py

+27
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,33 @@ def test_generate_tree_from_commits(gitcommits, tags):
796796
assert tuple(tree) == COMMITS_TREE
797797

798798

799+
@pytest.mark.parametrize(
800+
"change_type_order, expected_reordering",
801+
(
802+
([], {}),
803+
(
804+
["BREAKING CHANGE", "refactor"],
805+
{
806+
2: (["refactor", "feat", "fix"], ["feat", "fix", "refactor"]),
807+
3: (["BREAKING CHANGE", "refactor"], ["refactor", "BREAKING CHANGE"]),
808+
},
809+
),
810+
),
811+
)
812+
def test_order_changelog_tree(change_type_order, expected_reordering):
813+
tree = tuple(changelog.order_changelog_tree(COMMITS_TREE, change_type_order))
814+
815+
index_of_reordered_entry = [*expected_reordering.keys()]
816+
for index, entry in enumerate(tuple(tree)):
817+
if index in index_of_reordered_entry:
818+
sorted_order, original_order = expected_reordering[index]
819+
assert [*tree[index].keys()] == [*COMMITS_TREE[index].keys()]
820+
assert [*tree[index]["changes"].keys()] == sorted_order
821+
assert [*COMMITS_TREE[index]["changes"].keys()] == original_order
822+
else:
823+
assert [*entry["changes"].keys()] == [*tree[index]["changes"].keys()]
824+
825+
799826
def test_render_changelog(gitcommits, tags, changelog_content):
800827
parser = defaults.commit_parser
801828
changelog_pattern = defaults.bump_pattern

tests/test_cz_customize.py

+14
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
1414
bump_pattern = "^(break|new|fix|hotfix)"
1515
bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"}
16+
change_type_order = ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"]
1617
info = "This is a customized cz."
1718
1819
[[tool.commitizen.customize.questions]]
@@ -56,6 +57,7 @@
5657
"fix": "PATCH",
5758
"hotfix": "PATCH"
5859
},
60+
"change_type_order": ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"],
5961
"info": "This is a customized cz.",
6062
"questions": [
6163
{
@@ -107,6 +109,7 @@
107109
new: MINOR
108110
fix: PATCH
109111
hotfix: PATCH
112+
change_type_order: ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"]
110113
info: This is a customized cz.
111114
questions:
112115
- type: list
@@ -274,6 +277,17 @@ def test_bump_map(config):
274277
}
275278

276279

280+
def test_change_type_order(config):
281+
cz = CustomizeCommitsCz(config)
282+
assert cz.change_type_order == [
283+
"perf",
284+
"BREAKING CHANGE",
285+
"feat",
286+
"fix",
287+
"refactor",
288+
]
289+
290+
277291
def test_questions(config):
278292
cz = CustomizeCommitsCz(config)
279293
questions = cz.questions()

0 commit comments

Comments
 (0)