From 5b9837b9ba47a60e42807c150fa9db9a5b17714b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Tue, 12 May 2020 17:13:43 +0200 Subject: [PATCH 1/8] feat(changelog): add support for modifying the change_type in the title of the changelog --- commitizen/changelog.py | 5 ++- commitizen/commands/changelog.py | 13 +++++- commitizen/cz/base.py | 2 +- .../conventional_commits.py | 6 +++ .../templates/keep_a_changelog_template.j2 | 2 +- tests/CHANGELOG_FOR_TEST.md | 42 +++++++++---------- tests/test_changelog.py | 12 ++++++ 7 files changed, 56 insertions(+), 26 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index c701f4fcd9..bd3c0b1d6a 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -72,6 +72,7 @@ def generate_tree_from_commits( commit_parser: str, changelog_pattern: str = defaults.bump_pattern, unreleased_version: Optional[str] = None, + change_type_map: Optional[Dict[str, str]] = None, ) -> Iterable[Dict]: pat = re.compile(changelog_pattern) map_pat = re.compile(commit_parser) @@ -112,10 +113,12 @@ def generate_tree_from_commits( message = map_pat.match(commit.message) message_body = map_pat.match(commit.body) if message: - # TODO: add a post hook coming from a rule (CzBase) parsed_message: Dict = message.groupdict() # change_type becomes optional by providing None change_type = parsed_message.pop("change_type", None) + + if change_type_map: + change_type = change_type_map.get(change_type, change_type) changes[change_type].append(parsed_message) if message_body: parsed_message_body: Dict = message_body.groupdict() diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 0d6f8bb426..6ce8e0d0b3 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -1,7 +1,7 @@ import os.path from difflib import SequenceMatcher from operator import itemgetter -from typing import Dict, List +from typing import Dict, List, Optional from commitizen import changelog, factory, git, out from commitizen.config import BaseConfig @@ -27,6 +27,9 @@ def __init__(self, config: BaseConfig, args): self.incremental = args["incremental"] self.dry_run = args["dry_run"] self.unreleased_version = args["unreleased_version"] + self.change_type_map = ( + self.config.settings.get("change_type_map") or self.cz.change_type_map + ) def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str: """Try to find the 'start_rev'. @@ -60,6 +63,7 @@ def __call__(self): start_rev = self.start_rev unreleased_version = self.unreleased_version changelog_meta: Dict = {} + change_type_map: Optional[Dict] = self.change_type_map if not changelog_pattern or not commit_parser: out.error( @@ -83,7 +87,12 @@ def __call__(self): raise SystemExit(NO_COMMITS_FOUND) tree = changelog.generate_tree_from_commits( - commits, tags, commit_parser, changelog_pattern, unreleased_version + commits, + tags, + commit_parser, + changelog_pattern, + unreleased_version, + change_type_map, ) changelog_out = changelog.render_changelog(tree) diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index c6e74d95b0..3d129f638c 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -27,7 +27,7 @@ class BaseCommitizen(metaclass=ABCMeta): # It can be modified per rule commit_parser: Optional[str] = r"(?P.*)" changelog_pattern: Optional[str] = r".*" - changelog_map: Optional[dict] = None # TODO: Use it + change_type_map: Optional[dict] = None def __init__(self, config: BaseConfig): self.config = config diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index b932bd6682..e3fbb8c269 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -32,6 +32,12 @@ class ConventionalCommitsCz(BaseCommitizen): bump_map = defaults.bump_map commit_parser = defaults.commit_parser changelog_pattern = defaults.bump_pattern + change_type_map = { + "feat": "Feat", + "fix": "Fix", + "refactor": "Refactor", + "perf": "Perf", + } def questions(self) -> List[Dict[str, Any]]: questions: List[Dict[str, Any]] = [ diff --git a/commitizen/templates/keep_a_changelog_template.j2 b/commitizen/templates/keep_a_changelog_template.j2 index be0d0d26e6..381fe88bf3 100644 --- a/commitizen/templates/keep_a_changelog_template.j2 +++ b/commitizen/templates/keep_a_changelog_template.j2 @@ -5,7 +5,7 @@ {% for change_key, changes in entry.changes.items() %} {% if change_key %} -### {{ change_key|title }} +### {{ change_key }} {% endif %} {% for change in changes %} diff --git a/tests/CHANGELOG_FOR_TEST.md b/tests/CHANGELOG_FOR_TEST.md index 7605bbed47..798b07545a 100644 --- a/tests/CHANGELOG_FOR_TEST.md +++ b/tests/CHANGELOG_FOR_TEST.md @@ -1,13 +1,13 @@ ## v1.2.0 (2019-04-19) -### Feat +### feat - custom cz plugins now support bumping version ## v1.1.1 (2019-04-18) -### Refactor +### refactor - changed stdout statements - **schema**: command logic removed from commitizen base @@ -15,14 +15,14 @@ - **example**: command logic removed from commitizen base - **commit**: moved most of the commit logic to the commit command -### Fix +### fix - **bump**: commit message now fits better with semver - conventional commit 'breaking change' in body instead of title ## v1.1.0 (2019-04-14) -### Feat +### feat - new working bump command - create version tag @@ -31,22 +31,22 @@ - support for pyproject.toml - first semantic version bump implementaiton -### Fix +### fix - removed all from commit - fix config file not working -### Refactor +### refactor - added commands folder, better integration with decli ## v1.0.0 (2019-03-01) -### Refactor +### refactor - removed delegator, added decli and many tests -### Breaking Change +### BREAKING CHANGE - API is stable @@ -54,76 +54,76 @@ ## v1.0.0b1 (2019-01-17) -### Feat +### feat - py3 only, tests and conventional commits 1.0 ## v0.9.11 (2018-12-17) -### Fix +### fix - **config**: load config reads in order without failing if there is no commitizen section ## v0.9.10 (2018-09-22) -### Fix +### fix - parse scope (this is my punishment for not having tests) ## v0.9.9 (2018-09-22) -### Fix +### fix - parse scope empty ## v0.9.8 (2018-09-22) -### Fix +### fix - **scope**: parse correctly again ## v0.9.7 (2018-09-22) -### Fix +### fix - **scope**: parse correctly ## v0.9.6 (2018-09-19) -### Refactor +### refactor - **conventionalCommit**: moved fitlers to questions instead of message -### Fix +### fix - **manifest**: inluded missing files ## v0.9.5 (2018-08-24) -### Fix +### fix - **config**: home path for python versions between 3.0 and 3.5 ## v0.9.4 (2018-08-02) -### Feat +### feat - **cli**: added version ## v0.9.3 (2018-07-28) -### Feat +### feat - **commiter**: conventional commit is a bit more intelligent now ## v0.9.2 (2017-11-11) -### Refactor +### refactor - renamed conventional_changelog to conventional_commits, not backward compatible ## v0.9.1 (2017-11-11) -### Fix +### fix - **setup.py**: future is now required for every python version diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 94c3c47017..dace772e8f 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -701,3 +701,15 @@ def test_render_changelog_tag_and_unreleased(gitcommits, tags): assert "Unreleased" in result assert "## v1.1.1" in result + + +def test_render_changelog_with_change_type(gitcommits, tags, changelog_content): + new_title = ":some-emoji: feature" + change_type_map = {"feat": new_title} + parser = defaults.commit_parser + changelog_pattern = defaults.bump_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern, change_type_map=change_type_map + ) + result = changelog.render_changelog(tree) + assert new_title in result From 141981bb1bb14db50b6b3b5625a8b4c1ecd9ba4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Tue, 12 May 2020 17:14:04 +0200 Subject: [PATCH 2/8] docs: add info about creating custom changelog --- docs/changelog.md | 2 +- docs/customization.md | 61 +++++++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index fab38ea2ad..a5eaa5d343 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -127,7 +127,7 @@ cz changelog --incremental ## TODO - [ ] support for hooks: this would allow introduction of custom information in the commiter, like a github or jira url. Eventually we could build a `CzConventionalGithub`, which would add links to commits -- [ ] support for map: allow the usage of a `change_type` mapper, to convert from feat to feature for example. +- [x] support for map: allow the usage of a `change_type` mapper, to convert from feat to feature for example. [keepachangelog]: https://keepachangelog.com/ [semver]: https://semver.org/ diff --git a/docs/customization.md b/docs/customization.md index bae9f047af..2d9bcf5f27 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -106,12 +106,12 @@ If you feel like it should be part of this repo, create a PR. ### Custom bump rules -You need to define 2 parameters inside `BaseCommitizen`. +You need to define 2 parameters inside your custom `BaseCommitizen`. -| Parameter | Type | Default | Description | -| --------- | ---- | ------- | ----------- | -| `bump_pattern` | `str` | `None` | Regex to extract information from commit (subject and body) | -| `bump_map` | `dict` | `None` | Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) | +| Parameter | Type | Default | Description | +| -------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- | +| `bump_pattern` | `str` | `None` | Regex to extract information from commit (subject and body) | +| `bump_map` | `dict` | `None` | Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) | Let's see an example. @@ -132,6 +132,20 @@ cz -n cz_strange bump [convcomms]: https://github.com/commitizen-tools/commitizen/blob/master/commitizen/cz/conventional_commits/conventional_commits.py +## Custom changelog generator + +The changelog generator should just work in a very basic manner without touching anything. +You can customize it of course, and this are the variables you need to add to your custom `BaseCommitizen`. + +| 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 | +| `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-des]: ./changelog.md#description + ### Raise Customize Exception If you want `commitizen` to catch your exception and print the message, you'll have to inherit `CzException`. @@ -148,6 +162,7 @@ class NoSubjectProvidedException(CzException): **This is only supported when configuring through `toml` (e.g., `pyproject.toml`, `.cz`, and `.cz.toml`)** The basic steps are: + 1. Define your custom committing or bumping rules in the configuration file. 2. Declare `name = "cz_customize"` in your configuration file, or add `-n cz_customize` when running commitizen. @@ -187,24 +202,24 @@ message = "Do you want to add body message in commit?" ### Customize configuration -| Parameter | Type | Default | Description | -| --------- | ---- | ------- | ----------- | -| `question` | `dict` | `None` | Questions regarding the commit message. Detailed below. | -| `message_template` | `str` | `None` | The template for generating message from the given answers. `message_template` should either follow the [string.Template](https://docs.python.org/3/library/string.html#template-strings) or [Jinja2](https://jinja.palletsprojects.com/en/2.10.x/) formatting specification, and all the variables in this template should be defined in `name` in `questions`. Note that `Jinja2` is not installed by default. If not installed, commitizen will use `string.Template` formatting. | -| `example` | `str` | `None` | (OPTIONAL) Provide an example to help understand the style. Used by `cz example`. | -| `schema` | `str` | `None` | (OPTIONAL) Show the schema used. Used by `cz schema`. | -| `info_path` | `str` | `None` | (OPTIONAL) The path to the file that contains explanation of the commit rules. Used by `cz info`. If not provided `cz info`, will load `info` instead. | -| `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) | +| Parameter | Type | Default | Description | +| ------------------ | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `question` | `dict` | `None` | Questions regarding the commit message. Detailed below. | +| `message_template` | `str` | `None` | The template for generating message from the given answers. `message_template` should either follow the [string.Template](https://docs.python.org/3/library/string.html#template-strings) or [Jinja2](https://jinja.palletsprojects.com/en/2.10.x/) formatting specification, and all the variables in this template should be defined in `name` in `questions`. Note that `Jinja2` is not installed by default. If not installed, commitizen will use `string.Template` formatting. | +| `example` | `str` | `None` | (OPTIONAL) Provide an example to help understand the style. Used by `cz example`. | +| `schema` | `str` | `None` | (OPTIONAL) Show the schema used. Used by `cz schema`. | +| `info_path` | `str` | `None` | (OPTIONAL) The path to the file that contains explanation of the commit rules. Used by `cz info`. If not provided `cz info`, will load `info` instead. | +| `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) | #### Detailed `question` content -| Parameter | Type | Default | Description | -| --------- | ---- | ------- | ----------- | -| `type` | `str` | `None` | The type of questions. Valid type: `list`, `input` and etc. [See More](https://github.com/tmbo/questionary#different-question-types) | -| `name` | `str` | `None` | The key for the value answered by user. It's used in `message_template` | -| `message` | `str` | `None` | Detail description for the question. | -| `choices` | `list` | `None` | (OPTIONAL) The choices when `type = choice`. It should be list of dictionaries with `name` and `value`. (e.g., `[{value = "feature", name = "feature: A new feature."}, {value = "bug fix", name = "bug fix: A bug fix."}]`) | -| `default` | `Any` | `None` | (OPTIONAL) The default value for this question. | -| `filter` | `str` | `None` | (Optional) Validator for user's answer. **(Work in Progress)** | +| Parameter | Type | Default | Description | +| --------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | `str` | `None` | The type of questions. Valid type: `list`, `input` and etc. [See More](https://github.com/tmbo/questionary#different-question-types) | +| `name` | `str` | `None` | The key for the value answered by user. It's used in `message_template` | +| `message` | `str` | `None` | Detail description for the question. | +| `choices` | `list` | `None` | (OPTIONAL) The choices when `type = choice`. It should be list of dictionaries with `name` and `value`. (e.g., `[{value = "feature", name = "feature: A new feature."}, {value = "bug fix", name = "bug fix: A bug fix."}]`) | +| `default` | `Any` | `None` | (OPTIONAL) The default value for this question. | +| `filter` | `str` | `None` | (Optional) Validator for user's answer. **(Work in Progress)** | From e8b7ab1ce86037439d45f40bee12645cd4260bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Tue, 12 May 2020 17:32:37 +0200 Subject: [PATCH 3/8] feat(changelog): add support for `message_hook` method --- commitizen/changelog.py | 5 ++++- commitizen/commands/changelog.py | 6 ++++-- commitizen/cz/base.py | 3 ++- tests/test_changelog.py | 16 +++++++++++++++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index bd3c0b1d6a..8103116b56 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -29,7 +29,7 @@ import re from collections import defaultdict from datetime import date -from typing import Dict, Iterable, List, Optional +from typing import Callable, Dict, Iterable, List, Optional import pkg_resources from jinja2 import Template @@ -73,6 +73,7 @@ def generate_tree_from_commits( changelog_pattern: str = defaults.bump_pattern, unreleased_version: Optional[str] = None, change_type_map: Optional[Dict[str, str]] = None, + message_hook: Optional[Callable] = None, ) -> Iterable[Dict]: pat = re.compile(changelog_pattern) map_pat = re.compile(commit_parser) @@ -119,6 +120,8 @@ def generate_tree_from_commits( if change_type_map: change_type = change_type_map.get(change_type, change_type) + if message_hook: + parsed_message = message_hook(parsed_message, commit) changes[change_type].append(parsed_message) if message_body: parsed_message_body: Dict = message_body.groupdict() diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 6ce8e0d0b3..36799aff09 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -1,7 +1,7 @@ import os.path from difflib import SequenceMatcher from operator import itemgetter -from typing import Dict, List, Optional +from typing import Callable, Dict, List, Optional from commitizen import changelog, factory, git, out from commitizen.config import BaseConfig @@ -64,6 +64,7 @@ def __call__(self): unreleased_version = self.unreleased_version changelog_meta: Dict = {} change_type_map: Optional[Dict] = self.change_type_map + message_hook: Optional[Callable] = self.cz.message_hook if not changelog_pattern or not commit_parser: out.error( @@ -92,7 +93,8 @@ def __call__(self): commit_parser, changelog_pattern, unreleased_version, - change_type_map, + change_type_map=change_type_map, + message_hook=message_hook, ) changelog_out = changelog.render_changelog(tree) diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index 3d129f638c..435cb34e2d 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -1,5 +1,5 @@ from abc import ABCMeta, abstractmethod -from typing import List, Optional, Tuple +from typing import Callable, List, Optional, Tuple from prompt_toolkit.styles import Style, merge_styles @@ -28,6 +28,7 @@ class BaseCommitizen(metaclass=ABCMeta): commit_parser: Optional[str] = r"(?P.*)" changelog_pattern: Optional[str] = r".*" change_type_map: Optional[dict] = None + message_hook: Optional[Callable] = None # (dict, GitCommit) -> dict def __init__(self, config: BaseConfig): self.config = config diff --git a/tests/test_changelog.py b/tests/test_changelog.py index dace772e8f..0a11810ecb 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -703,7 +703,7 @@ def test_render_changelog_tag_and_unreleased(gitcommits, tags): assert "## v1.1.1" in result -def test_render_changelog_with_change_type(gitcommits, tags, changelog_content): +def test_render_changelog_with_change_type(gitcommits, tags): new_title = ":some-emoji: feature" change_type_map = {"feat": new_title} parser = defaults.commit_parser @@ -713,3 +713,17 @@ def test_render_changelog_with_change_type(gitcommits, tags, changelog_content): ) result = changelog.render_changelog(tree) assert new_title in result + + +def test_render_changelog_with_message_hook(gitcommits, tags): + def message_hook(message: dict, _) -> dict: + message["message"] = f"{message['message']} [link](github.com/232323232)" + return message + + parser = defaults.commit_parser + changelog_pattern = defaults.bump_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern, message_hook=message_hook + ) + result = changelog.render_changelog(tree) + assert "[link](github.com/232323232)" in result From 221a8d75a539063ca73d095f6f475d32aa8b9a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Tue, 12 May 2020 17:33:20 +0200 Subject: [PATCH 4/8] docs(changelog): document usage of message_hook --- docs/customization.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 2d9bcf5f27..8566db06e7 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -137,12 +137,12 @@ cz -n cz_strange bump The changelog generator should just work in a very basic manner without touching anything. You can customize it of course, and this are the variables you need to add to your custom `BaseCommitizen`. -| 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 | -| `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 | - +| 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 | +| `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 | +| `message_hook` | `method: (dict, git.GitCommit) -> dict` | NO | Customize with extra information your message output, like adding links, this function is executed per parsed commit. | [changelog-des]: ./changelog.md#description From 87bebaf00c405683d5a243ecc0d38481184ade75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Tue, 12 May 2020 18:25:41 +0200 Subject: [PATCH 5/8] feat(changelog): add support for `changelog_hook` when changelog finishes the generation --- commitizen/commands/changelog.py | 11 ++++++++--- commitizen/cz/base.py | 12 +++++++++--- setup.cfg | 2 +- tests/commands/test_changelog_command.py | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 36799aff09..50564bb4ae 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -65,6 +65,7 @@ def __call__(self): changelog_meta: Dict = {} change_type_map: Optional[Dict] = self.change_type_map message_hook: Optional[Callable] = self.cz.message_hook + changelog_hook: Optional[Callable] = self.cz.changelog_hook if not changelog_pattern or not commit_parser: out.error( @@ -108,10 +109,14 @@ def __call__(self): lines = changelog_file.readlines() with open(self.file_name, "w") as changelog_file: + partial_changelog: Optional[str] = None if self.incremental: new_lines = changelog.incremental_build( changelog_out, lines, changelog_meta ) - changelog_file.writelines(new_lines) - else: - changelog_file.write(changelog_out) + changelog_out = "".join(new_lines) + partial_changelog = changelog_out + + if changelog_hook: + changelog_out = changelog_hook(changelog_out, partial_changelog) + changelog_file.write(changelog_out) diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index 435cb34e2d..8c3a082189 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -1,8 +1,9 @@ from abc import ABCMeta, abstractmethod -from typing import Callable, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple from prompt_toolkit.styles import Style, merge_styles +from commitizen import git from commitizen.config.base_config import BaseConfig @@ -27,8 +28,13 @@ class BaseCommitizen(metaclass=ABCMeta): # It can be modified per rule commit_parser: Optional[str] = r"(?P.*)" changelog_pattern: Optional[str] = r".*" - change_type_map: Optional[dict] = None - message_hook: Optional[Callable] = None # (dict, GitCommit) -> dict + change_type_map: Optional[Dict[str, str]] = None + + # Executed per message parsed by the commitizen + message_hook: Optional[Callable[[Dict, git.GitCommit], Dict]] = None + + # Executed only at the end of the changelog generation + changelog_hook: Optional[Callable[[str, Optional[str]], str]] = None def __init__(self, config: BaseConfig): self.config = config diff --git a/setup.cfg b/setup.cfg index ac1f11ee38..de0172c175 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,4 +35,4 @@ exclude = build, dist max-line-length = 88 -max-complexity = 10 +max-complexity = 11 diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index d78d04bee2..0c7982ea6c 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -5,6 +5,7 @@ import pytest from commitizen import cli, git +from commitizen.commands.changelog import Changelog from tests.utils import create_file_and_commit @@ -234,3 +235,22 @@ def test_changlog_incremental_keep_a_changelog_sample(mocker, capsys): out == """# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n\n## Unreleased \n\n### Feat\n\n- add more stuff\n- add new output\n\n### Fix\n\n- mama gotta work\n- output glitch\n\n## [1.0.0] - 2017-06-20\n### Added\n- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8).\n- Version navigation.\n\n### Changed\n- Start using "changelog" over "change log" since it\'s the common usage.\n\n### Removed\n- Section about "changelog" vs "CHANGELOG".\n\n## [0.3.0] - 2015-12-03\n### Added\n- RU translation from [@aishek](https://github.com/aishek).\n""" ) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_hook(mocker, config): + changelog_hook_mock = mocker.Mock() + changelog_hook_mock.return_value = "cool changelog hook" + + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: is in changelog") + create_file_and_commit("Merge into master") + + changelog = Changelog( + config, {"unreleased_version": None, "incremental": True, "dry_run": False,}, + ) + mocker.patch.object(changelog.cz, "changelog_hook", changelog_hook_mock) + changelog() + full_changelog = "\n## Unreleased \n\n### Refactor\n\n- is in changelog\n\n### Feat\n\n- new file\n" + + changelog_hook_mock.assert_called_with(full_changelog, full_changelog) From fb73367393c05f9ed34c4bf4683fcf38e6818a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Tue, 12 May 2020 18:26:08 +0200 Subject: [PATCH 6/8] docs: add info about creating hooks --- docs/changelog.md | 11 +++-- docs/customization.md | 52 +++++++++++++++++++++--- tests/commands/test_changelog_command.py | 2 +- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index a5eaa5d343..2c78761324 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -124,10 +124,15 @@ Benefits: cz changelog --incremental ``` -## TODO +## Hooks -- [ ] support for hooks: this would allow introduction of custom information in the commiter, like a github or jira url. Eventually we could build a `CzConventionalGithub`, which would add links to commits -- [x] support for map: allow the usage of a `change_type` mapper, to convert from feat to feature for example. +Supported hook methods: + +- per parsed message: useful to add links +- end of changelog generation: useful to send slack or chat message, or notify another department + +Read more about hooks in the [customization page][customization] [keepachangelog]: https://keepachangelog.com/ [semver]: https://semver.org/ +[customization]: ./customization.md diff --git a/docs/customization.md b/docs/customization.md index 8566db06e7..860101cb5c 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -137,12 +137,52 @@ cz -n cz_strange bump The changelog generator should just work in a very basic manner without touching anything. You can customize it of course, and this are the variables you need to add to your custom `BaseCommitizen`. -| 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 | -| `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 | -| `message_hook` | `method: (dict, git.GitCommit) -> dict` | NO | Customize with extra information your message output, like adding links, this function is executed per parsed commit. | +| 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 | +| `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 | +| `message_hook` | `method: (dict, git.GitCommit) -> dict` | NO | Customize with extra information your message output, like adding links, this function is executed per parsed commit. | +| `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 | + +```python +from commitizen.cz.base import BaseCommitizen +import chat +import compliance + +class StrangeCommitizen(BaseCommitizen): + changelog_pattern = r"^(break|new|fix|hotfix)" + commit_parser = r"^(?Pfeat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?:\s(?P.*)?" + change_type_map = { + "feat": "Features", + "fix": "Bug Fixes", + "refactor": "Code Refactor", + "perf": "Performance improvements" + } + + def message_hook(self, parsed_message: dict, commit: git.GitCommit) -> dict: + rev = commit.rev + m = parsed_message["message"] + parsed_message["message"] = f"{m} {rev}" + return parsed_message + + def changelog_hook(self, full_changelog: str, partial_changelog: Optional[str]) -> str: + """Executed at the end of the changelog generation + + full_changelog: it's the output about to being written into the file + partial_changelog: it's the new stuff, this is useful to send slack messages or + similar + + Return: + the new updated full_changelog + """ + if partial_changelog: + chat.room("#commiters").notify(partial_changelog) + if full_changelog: + compliance.send(full_changelog) + full_changelog.replace(' fix ', ' **fix** ') + return full_changelog +``` [changelog-des]: ./changelog.md#description diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index 0c7982ea6c..a676295c4c 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -247,7 +247,7 @@ def test_changlog_hook(mocker, config): create_file_and_commit("Merge into master") changelog = Changelog( - config, {"unreleased_version": None, "incremental": True, "dry_run": False,}, + config, {"unreleased_version": None, "incremental": True, "dry_run": False}, ) mocker.patch.object(changelog.cz, "changelog_hook", changelog_hook_mock) changelog() From aa2a9f9f9a11d63ef03f75a128fb504c12cad385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Tue, 12 May 2020 18:52:20 +0200 Subject: [PATCH 7/8] style: align with flake8 newest version --- commitizen/commands/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commitizen/commands/version.py b/commitizen/commands/version.py index 344920c72a..143a0c7351 100644 --- a/commitizen/commands/version.py +++ b/commitizen/commands/version.py @@ -16,14 +16,14 @@ def __call__(self): if version: out.write(f"{version}") else: - out.error(f"No project information in this project.") + out.error("No project information in this project.") elif self.parameter.get("verbose"): out.write(f"Installed Commitizen Version: {__version__}") version = self.config.settings["version"] if version: out.write(f"Project Version: {version}") else: - out.error(f"No project information in this project.") + out.error("No project information in this project.") else: # if no argument is given, show installed commitizen version out.write(f"{__version__}") From df0042cbf236bda750c1d4b26250d16f7708f8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Wed, 13 May 2020 12:27:53 +0200 Subject: [PATCH 8/8] fix(changelog): rename `message_hook` -> `changelog_message_builder_hook` --- commitizen/changelog.py | 6 +++--- commitizen/commands/changelog.py | 6 ++++-- commitizen/cz/base.py | 4 +++- docs/customization.md | 4 ++-- tests/test_changelog.py | 10 +++++++--- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 8103116b56..bde2d51763 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -73,7 +73,7 @@ def generate_tree_from_commits( changelog_pattern: str = defaults.bump_pattern, unreleased_version: Optional[str] = None, change_type_map: Optional[Dict[str, str]] = None, - message_hook: Optional[Callable] = None, + changelog_message_builder_hook: Optional[Callable] = None, ) -> Iterable[Dict]: pat = re.compile(changelog_pattern) map_pat = re.compile(commit_parser) @@ -120,8 +120,8 @@ def generate_tree_from_commits( if change_type_map: change_type = change_type_map.get(change_type, change_type) - if message_hook: - parsed_message = message_hook(parsed_message, commit) + if changelog_message_builder_hook: + parsed_message = changelog_message_builder_hook(parsed_message, commit) changes[change_type].append(parsed_message) if message_body: parsed_message_body: Dict = message_body.groupdict() diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 50564bb4ae..93cd26d793 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -64,7 +64,9 @@ def __call__(self): unreleased_version = self.unreleased_version changelog_meta: Dict = {} change_type_map: Optional[Dict] = self.change_type_map - message_hook: Optional[Callable] = self.cz.message_hook + changelog_message_builder_hook: Optional[ + Callable + ] = self.cz.changelog_message_builder_hook changelog_hook: Optional[Callable] = self.cz.changelog_hook if not changelog_pattern or not commit_parser: @@ -95,7 +97,7 @@ def __call__(self): changelog_pattern, unreleased_version, change_type_map=change_type_map, - message_hook=message_hook, + changelog_message_builder_hook=changelog_message_builder_hook, ) changelog_out = changelog.render_changelog(tree) diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index 8c3a082189..ddf0c7997c 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -31,7 +31,9 @@ class BaseCommitizen(metaclass=ABCMeta): change_type_map: Optional[Dict[str, str]] = None # Executed per message parsed by the commitizen - message_hook: Optional[Callable[[Dict, git.GitCommit], Dict]] = None + changelog_message_builder_hook: Optional[ + Callable[[Dict, git.GitCommit], Dict] + ] = None # Executed only at the end of the changelog generation changelog_hook: Optional[Callable[[str, Optional[str]], str]] = None diff --git a/docs/customization.md b/docs/customization.md index 860101cb5c..721ade60b3 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -142,7 +142,7 @@ You can customize it of course, and this are the variables you need to add to yo | `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 | | `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 | -| `message_hook` | `method: (dict, git.GitCommit) -> dict` | NO | Customize with extra information your message output, like adding links, this function is executed per parsed commit. | +| `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. | | `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 | ```python @@ -160,7 +160,7 @@ class StrangeCommitizen(BaseCommitizen): "perf": "Performance improvements" } - def message_hook(self, parsed_message: dict, commit: git.GitCommit) -> dict: + def changelog_message_builder_hook(self, parsed_message: dict, commit: git.GitCommit) -> dict: rev = commit.rev m = parsed_message["message"] parsed_message["message"] = f"{m} {rev}" diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 0a11810ecb..29b644619f 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -715,15 +715,19 @@ def test_render_changelog_with_change_type(gitcommits, tags): assert new_title in result -def test_render_changelog_with_message_hook(gitcommits, tags): - def message_hook(message: dict, _) -> dict: +def test_render_changelog_with_changelog_message_builder_hook(gitcommits, tags): + def changelog_message_builder_hook(message: dict, _) -> dict: message["message"] = f"{message['message']} [link](github.com/232323232)" return message parser = defaults.commit_parser changelog_pattern = defaults.bump_pattern tree = changelog.generate_tree_from_commits( - gitcommits, tags, parser, changelog_pattern, message_hook=message_hook + gitcommits, + tags, + parser, + changelog_pattern, + changelog_message_builder_hook=changelog_message_builder_hook, ) result = changelog.render_changelog(tree) assert "[link](github.com/232323232)" in result