From d1cac309270b30f3b26dc3aec91e72a62ee824f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Sat, 20 Jul 2019 13:01:07 +0200 Subject: [PATCH 01/35] feat(changelog): changelog tree generation from markdown --- commitizen/changelog.py | 133 +++++++++++++++++++++ commitizen/cli.py | 5 + commitizen/commands/__init__.py | 4 + commitizen/commands/changelog.py | 14 +++ tests/test_changelog.py | 199 +++++++++++++++++++++++++++++++ 5 files changed, 355 insertions(+) create mode 100644 commitizen/changelog.py create mode 100644 commitizen/commands/changelog.py create mode 100644 tests/test_changelog.py diff --git a/commitizen/changelog.py b/commitizen/changelog.py new file mode 100644 index 0000000000..495a4513c4 --- /dev/null +++ b/commitizen/changelog.py @@ -0,0 +1,133 @@ +""" +# DESIGN + +## Parse CHANGELOG.md + +1. Get LATEST VERSION from CONFIG +1. Parse the file version to version +2. Build a dict (tree) of that particular version +3. Transform tree into markdown again + +## Parse git log + +1. get commits between versions +2. filter commits with the current cz rules +3. parse commit information +4. generate tree + +Options: +- Generate full or partial changelog +""" +from typing import Generator, List, Dict, Iterable +import re + +MD_VERSION_RE = r"^##\s(?P[a-zA-Z0-9.+]+)\s?\(?(?P[0-9-]+)?\)?" +MD_CATEGORY_RE = r"^###\s(?P[a-zA-Z0-9.+\s]+)" +MD_MESSAGE_RE = r"^-\s(\*{2}(?P[a-zA-Z0-9]+)\*{2}:\s)?(?P.+)" +md_version_c = re.compile(MD_VERSION_RE) +md_category_c = re.compile(MD_CATEGORY_RE) +md_message_c = re.compile(MD_MESSAGE_RE) + + +CATEGORIES = [ + ("fix", "fix"), + ("breaking", "BREAKING CHANGES"), + ("feat", "feat"), + ("refactor", "refactor"), + ("perf", "perf"), + ("test", "test"), + ("build", "build"), + ("ci", "ci"), + ("chore", "chore"), +] + + +def find_version_blocks(filepath: str) -> Generator: + """ + version block: contains all the information about a version. + + E.g: + ``` + ## 1.2.1 (2019-07-20) + + ## Bug fixes + + - username validation not working + + ## Features + + - new login system + + ``` + """ + with open(filepath, "r") as f: + block: list = [] + for line in f: + line = line.strip("\n") + if not line: + continue + + if line.startswith("## "): + if len(block) > 0: + yield block + block = [line] + else: + block.append(line) + yield block + + +def parse_md_version(md_version: str) -> Dict: + m = md_version_c.match(md_version) + if not m: + return {} + return m.groupdict() + + +def parse_md_category(md_category: str) -> Dict: + m = md_category_c.match(md_category) + if not m: + return {} + return m.groupdict() + + +def parse_md_message(md_message: str) -> Dict: + m = md_message_c.match(md_message) + if not m: + return {} + return m.groupdict() + + +def transform_category(category: str) -> str: + _category_lower = category.lower() + for match_value, output in CATEGORIES: + if re.search(match_value, _category_lower): + return output + else: + raise ValueError(f"Could not match a category with {category}") + + +def generate_block_tree(block: List[str]) -> Dict: + tree: Dict = {"commits": []} + category = None + for line in block: + if line.startswith("## "): + category = None + tree = {**tree, **parse_md_version(line)} + elif line.startswith("### "): + result = parse_md_category(line) + if not result: + continue + category = transform_category(result.get("category", "")) + + elif line.startswith("- "): + commit = parse_md_message(line) + commit["category"] = category + tree["commits"].append(commit) + else: + print("it's something else: ", line) + return tree + + +def generate_full_tree(blocks: Iterable) -> Iterable[Dict]: + for block in blocks: + yield generate_block_tree(block) diff --git a/commitizen/cli.py b/commitizen/cli.py index 4ea45d431b..352e83f93f 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -38,6 +38,11 @@ "help": "show available commitizens", "func": commands.ListCz, }, + { + "name": ["changelog", "ch"], + "help": "create new changelog", + "func": commands.Changelog, + }, { "name": ["commit", "c"], "help": "create new commit", diff --git a/commitizen/commands/__init__.py b/commitizen/commands/__init__.py index b87906d8b4..6d1d8a13e5 100644 --- a/commitizen/commands/__init__.py +++ b/commitizen/commands/__init__.py @@ -7,11 +7,15 @@ from .list_cz import ListCz from .schema import Schema from .version import Version +from .init import Init +from .changelog import Changelog + __all__ = ( "Bump", "Check", "Commit", + "Changelog" "Example", "Info", "ListCz", diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py new file mode 100644 index 0000000000..f859292125 --- /dev/null +++ b/commitizen/commands/changelog.py @@ -0,0 +1,14 @@ +from commitizen import factory, out, changelog + + +class Changelog: + """Generate a changelog based on the commit history.""" + + def __init__(self, config: dict, *args): + self.config: dict = config + self.cz = factory.commiter_factory(self.config) + + def __call__(self): + self.config + out.write("changelog") + changelog diff --git a/tests/test_changelog.py b/tests/test_changelog.py new file mode 100644 index 0000000000..1d8e129079 --- /dev/null +++ b/tests/test_changelog.py @@ -0,0 +1,199 @@ +import os + + +import pytest + +from commitizen import changelog + + +COMMIT_LOG = [ + "bump: version 1.5.0 → 1.5.1", + "", + "Merge pull request #29 from esciara/issue_28", + "fix: #28 allows poetry add on py36 envs", + "fix: #28 allows poetry add on py36 envs", + "", + "Merge pull request #26 from Woile/dependabot/pip/black-tw-19.3b0", + "chore(deps-dev): update black requirement from ^18.3-alpha.0 to ^19.3b0", + "Merge pull request #27 from Woile/dependabot/pip/mypy-tw-0.701", + "chore(deps-dev): update mypy requirement from ^0.700.0 to ^0.701", + "chore(deps-dev): update mypy requirement from ^0.700.0 to ^0.701", + "Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.", + "- [Release notes](https://github.com/python/mypy/releases)", + "- [Commits](https://github.com/python/mypy/compare/v0.700...v0.701)", + "", + "Signed-off-by: dependabot[bot] ", + "chore(deps-dev): update black requirement from ^18.3-alpha.0 to ^19.3b0", + "Updates the requirements on [black](https://github.com/ambv/black) to permit the latest version.", + "- [Release notes](https://github.com/ambv/black/releases)", + "- [Commits](https://github.com/ambv/black/commits)", + "", + "Signed-off-by: dependabot[bot] ", + "bump: version 1.4.0 → 1.5.0", + "", + "docs: add info about extra pattern in the files when bumping", + "", + "feat(bump): it is now possible to specify a pattern in the files attr to replace the version", + "", +] + +CHANGELOG_TEMPLATE = """ +## 1.0.0 (2019-07-12) + +### Bug fixes + +- issue in poetry add preventing the installation in py36 +- **users**: lorem ipsum apap + + +### Features + +- it is possible to specify a pattern to be matched in configuration files bump. + +## 0.9 (2019-07-11) + +### Bug fixes + +- holis + +""" + + +@pytest.fixture +def existing_changelog_file(request): + changelog_path = "tests/CHANGELOG.md" + + with open(changelog_path, "w") as f: + f.write(CHANGELOG_TEMPLATE) + + yield changelog_path + + os.remove(changelog_path) + + +def test_read_changelog_blocks(existing_changelog_file): + blocks = changelog.find_version_blocks(existing_changelog_file) + blocks = list(blocks) + amount_of_blocks = len(blocks) + assert amount_of_blocks == 2 + + +VERSION_CASES: list = [ + ("## 1.0.0 (2019-07-12)", {"version": "1.0.0", "date": "2019-07-12"}), + ("## 2.3.0a0", {"version": "2.3.0a0", "date": None}), + ("## 0.10.0a0", {"version": "0.10.0a0", "date": None}), + ("## 1.0.0rc0", {"version": "1.0.0rc0", "date": None}), + ("## 1beta", {"version": "1beta", "date": None}), + ( + "## 1.0.0rc1+e20d7b57f3eb (2019-3-24)", + {"version": "1.0.0rc1+e20d7b57f3eb", "date": "2019-3-24"}, + ), + ("### Bug fixes", {}), + ("- issue in poetry add preventing the installation in py36", {}), +] + +CATEGORIES_CASES: list = [ + ("## 1.0.0 (2019-07-12)", {}), + ("## 2.3.0a0", {}), + ("### Bug fixes", {"category": "Bug fixes"}), + ("### Features", {"category": "Features"}), + ("- issue in poetry add preventing the installation in py36", {}), +] +CATEGORIES_TRANSFORMATIONS: list = [ + ("Bug fixes", "fix"), + ("Features", "feat"), + ("BREAKING CHANGES", "BREAKING CHANGES"), +] + +MESSAGES_CASES: list = [ + ("## 1.0.0 (2019-07-12)", {}), + ("## 2.3.0a0", {}), + ("### Bug fixes", {}), + ( + "- name no longer accept invalid chars", + {"message": "name no longer accept invalid chars", "scope": None}, + ), + ( + "- **users**: lorem ipsum apap", + {"message": "lorem ipsum apap", "scope": "users"}, + ), +] + + +@pytest.mark.parametrize("test_input,expected", VERSION_CASES) +def test_parse_md_version(test_input, expected): + assert changelog.parse_md_version(test_input) == expected + + +@pytest.mark.parametrize("test_input,expected", CATEGORIES_CASES) +def test_parse_md_category(test_input, expected): + assert changelog.parse_md_category(test_input) == expected + + +@pytest.mark.parametrize("test_input,expected", CATEGORIES_TRANSFORMATIONS) +def test_transform_category(test_input, expected): + assert changelog.transform_category(test_input) == expected + + +@pytest.mark.parametrize("test_input,expected", MESSAGES_CASES) +def test_parse_md_message(test_input, expected): + assert changelog.parse_md_message(test_input) == expected + + +def test_transform_category_fail(): + with pytest.raises(ValueError) as excinfo: + changelog.transform_category("Bugs") + assert "Could not match a category" in str(excinfo.value) + + +def test_generate_block_tree(existing_changelog_file): + blocks = changelog.find_version_blocks(existing_changelog_file) + block = next(blocks) + tree = changelog.generate_block_tree(block) + assert tree == { + "commits": [ + { + "scope": None, + "message": "issue in poetry add preventing the installation in py36", + "category": "fix", + }, + {"scope": "users", "message": "lorem ipsum apap", "category": "fix"}, + { + "scope": None, + "message": "it is possible to specify a pattern to be matched in configuration files bump.", + "category": "feat", + }, + ], + "version": "1.0.0", + "date": "2019-07-12", + } + + +def test_generate_full_tree(existing_changelog_file): + blocks = changelog.find_version_blocks(existing_changelog_file) + tree = list(changelog.generate_full_tree(blocks)) + + assert tree == [ + { + "commits": [ + { + "scope": None, + "message": "issue in poetry add preventing the installation in py36", + "category": "fix", + }, + {"scope": "users", "message": "lorem ipsum apap", "category": "fix"}, + { + "scope": None, + "message": "it is possible to specify a pattern to be matched in configuration files bump.", + "category": "feat", + }, + ], + "version": "1.0.0", + "date": "2019-07-12", + }, + { + "commits": [{"scope": None, "message": "holis", "category": "fix"}], + "version": "0.9", + "date": "2019-07-11", + }, + ] From 9326693fbc51bef6068632ea0b72fcf37b88083d Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 15 Jan 2020 19:42:22 +0800 Subject: [PATCH 02/35] feat(cz/base): add default process_commit for processing commit message --- commitizen/cz/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index cd0775e6f7..d41b876c58 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -59,3 +59,10 @@ def schema_pattern(self) -> Optional[str]: def info(self) -> Optional[str]: """Information about the standardized commit message.""" raise NotImplementedError("Not Implemented yet") + + def process_commit(self, commit: str) -> str: + """Process commit for changelog. + + If not overwritten, it returns the first line of commit. + """ + return commit.split("\n")[0] From 0c3b666efe5c3b06ce4fe63110f37495e7412d64 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 15 Jan 2020 19:43:05 +0800 Subject: [PATCH 03/35] feat(cz/conventinal_commits): add changelog_map, changelog_pattern and implement process_commit --- .../cz/conventional_commits/conventional_commits.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index fcf0df4b58..c785977b0b 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -1,4 +1,6 @@ import os +import re +from collections import OrderedDict from typing import Any, Dict, List from commitizen import defaults @@ -29,6 +31,10 @@ def parse_subject(text): class ConventionalCommitsCz(BaseCommitizen): bump_pattern = defaults.bump_pattern bump_map = defaults.bump_map + changelog_pattern = r"^(BREAKING CHANGE|feat|fix)" + changelog_map = OrderedDict( + {"BREAKING CHANGES": "breaking", "feat": "feat", "fix": "fix"} + ) def questions(self) -> List[Dict[str, Any]]: questions: List[Dict[str, Any]] = [ @@ -183,3 +189,8 @@ def info(self) -> str: with open(filepath, "r") as f: content = f.read() return content + + def process_commit(self, commit: str) -> str: + pat = re.compile(self.schema_pattern()) + m = re.match(pat, commit) + return m.group(3).strip() From cda6be415e89dd12eb70b0945cd96b65e2fd89d0 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 15 Jan 2020 19:44:24 +0800 Subject: [PATCH 04/35] feat(commands/changelog): generate changelog_tree from all past commits TODO: find rev for each changelog entry --- commitizen/commands/changelog.py | 43 +++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index f859292125..9c5591254c 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -1,14 +1,45 @@ -from commitizen import factory, out, changelog +import re +from collections import OrderedDict + +from commitizen import factory, out, git +from commitizen.config import BaseConfig class Changelog: """Generate a changelog based on the commit history.""" - def __init__(self, config: dict, *args): - self.config: dict = config + def __init__(self, config: BaseConfig, *args): + self.config: BaseConfig = config self.cz = factory.commiter_factory(self.config) + # TODO: make these argument + self.skip_merge = True + def __call__(self): - self.config - out.write("changelog") - changelog + changelog_map = self.cz.changelog_map + changelog_pattern = self.cz.changelog_pattern + if not changelog_map: + out.error( + f"'{self.config.settings['name']}' rule does not support changelog" + ) + + pat = re.compile(changelog_pattern) + + changelog_tree = OrderedDict({value: [] for value in changelog_map.values()}) + commits = git.get_commits() + for commit in commits: + if self.skip_merge and commit.startswith("Merge"): + continue + + for message in commit.split("\n"): + result = pat.search(message) + if not result: + continue + found_keyword = result.group(0) + processed_commit = self.cz.process_commit(commit) + changelog_tree[changelog_map[found_keyword]].append(processed_commit) + break + + # TODO: handle rev + # an entry of changelog contains 'rev -> change_type -> message' + # the code above handles `change_type -> message` part From dee82859d00833dfbfc8b2319d5e33d8ad45afc1 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 17 Jan 2020 16:31:04 +0800 Subject: [PATCH 05/35] feat(changelog): generate changelog based on git log it will over write the existing file --- commitizen/commands/changelog.py | 40 +++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 9c5591254c..d9dfff89b7 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -12,8 +12,9 @@ def __init__(self, config: BaseConfig, *args): self.config: BaseConfig = config self.cz = factory.commiter_factory(self.config) - # TODO: make these argument + # TODO: make these attribute arguments self.skip_merge = True + self.file_name = "CHANGELOG.md" def __call__(self): changelog_map = self.cz.changelog_map @@ -25,21 +26,42 @@ def __call__(self): pat = re.compile(changelog_pattern) - changelog_tree = OrderedDict({value: [] for value in changelog_map.values()}) + changelog_entry_key = "Unreleased" + changelog_entry_values = OrderedDict({value: [] for value in changelog_map.values()}) commits = git.get_commits() + tag_map = {tag.rev: tag.name for tag in git.get_tags()} + + changelog_str = "# Changelog\n" for commit in commits: - if self.skip_merge and commit.startswith("Merge"): + if self.skip_merge and commit.message.startswith("Merge"): continue - for message in commit.split("\n"): + if commit.rev in tag_map: + changelog_str += f"\n## {changelog_entry_key}\n" + for key, values in changelog_entry_values.items(): + if not values: + continue + changelog_str += f"* {key}\n" + for value in values: + changelog_str += f" * {value}\n" + changelog_entry_key = tag_map[commit.rev] + + for message in commit.message.split("\n"): result = pat.search(message) if not result: continue found_keyword = result.group(0) - processed_commit = self.cz.process_commit(commit) - changelog_tree[changelog_map[found_keyword]].append(processed_commit) + processed_commit = self.cz.process_commit(commit.message) + changelog_entry_values[changelog_map[found_keyword]].append(processed_commit) break - # TODO: handle rev - # an entry of changelog contains 'rev -> change_type -> message' - # the code above handles `change_type -> message` part + changelog_str += f"\n## {changelog_entry_key}\n" + for key, values in changelog_entry_values.items(): + if not values: + continue + changelog_str += f"* {key}\n" + for value in values: + changelog_str += f" * {value}\n" + + with open(self.file_name, "w") as changelog_file: + changelog_file.write(changelog_str) From b5eb128fbd2f387a419654d547ed7ce0439d6653 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 15 Jan 2020 16:11:31 +0800 Subject: [PATCH 06/35] style(all): blackify --- commitizen/commands/__init__.py | 2 +- commitizen/commands/changelog.py | 8 ++++++-- tests/test_changelog.py | 29 +++++++++++++++++++++++------ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/commitizen/commands/__init__.py b/commitizen/commands/__init__.py index 6d1d8a13e5..fde589f8df 100644 --- a/commitizen/commands/__init__.py +++ b/commitizen/commands/__init__.py @@ -15,7 +15,7 @@ "Bump", "Check", "Commit", - "Changelog" + "Changelog", "Example", "Info", "ListCz", diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index d9dfff89b7..54554c170c 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -27,7 +27,9 @@ def __call__(self): pat = re.compile(changelog_pattern) changelog_entry_key = "Unreleased" - changelog_entry_values = OrderedDict({value: [] for value in changelog_map.values()}) + changelog_entry_values = OrderedDict( + {value: [] for value in changelog_map.values()} + ) commits = git.get_commits() tag_map = {tag.rev: tag.name for tag in git.get_tags()} @@ -52,7 +54,9 @@ def __call__(self): continue found_keyword = result.group(0) processed_commit = self.cz.process_commit(commit.message) - changelog_entry_values[changelog_map[found_keyword]].append(processed_commit) + changelog_entry_values[changelog_map[found_keyword]].append( + processed_commit + ) break changelog_str += f"\n## {changelog_entry_key}\n" diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 1d8e129079..ba0a730859 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -18,13 +18,19 @@ "Merge pull request #27 from Woile/dependabot/pip/mypy-tw-0.701", "chore(deps-dev): update mypy requirement from ^0.700.0 to ^0.701", "chore(deps-dev): update mypy requirement from ^0.700.0 to ^0.701", - "Updates the requirements on [mypy](https://github.com/python/mypy) to permit the latest version.", + ( + "Updates the requirements on " + "[mypy](https://github.com/python/mypy) to permit the latest version." + ), "- [Release notes](https://github.com/python/mypy/releases)", "- [Commits](https://github.com/python/mypy/compare/v0.700...v0.701)", "", "Signed-off-by: dependabot[bot] ", "chore(deps-dev): update black requirement from ^18.3-alpha.0 to ^19.3b0", - "Updates the requirements on [black](https://github.com/ambv/black) to permit the latest version.", + ( + "Updates the requirements on [black](https://github.com/ambv/black)" + " to permit the latest version." + ), "- [Release notes](https://github.com/ambv/black/releases)", "- [Commits](https://github.com/ambv/black/commits)", "", @@ -33,7 +39,10 @@ "", "docs: add info about extra pattern in the files when bumping", "", - "feat(bump): it is now possible to specify a pattern in the files attr to replace the version", + ( + "feat(bump): it is now possible to specify a pattern " + "in the files attr to replace the version" + ), "", ] @@ -160,7 +169,10 @@ def test_generate_block_tree(existing_changelog_file): {"scope": "users", "message": "lorem ipsum apap", "category": "fix"}, { "scope": None, - "message": "it is possible to specify a pattern to be matched in configuration files bump.", + "message": ( + "it is possible to specify a pattern to be matched " + "in configuration files bump." + ), "category": "feat", }, ], @@ -178,13 +190,18 @@ def test_generate_full_tree(existing_changelog_file): "commits": [ { "scope": None, - "message": "issue in poetry add preventing the installation in py36", + "message": ( + "issue in poetry add preventing the installation in py36" + ), "category": "fix", }, {"scope": "users", "message": "lorem ipsum apap", "category": "fix"}, { "scope": None, - "message": "it is possible to specify a pattern to be matched in configuration files bump.", + "message": ( + "it is possible to specify a pattern to be matched " + "in configuration files bump." + ), "category": "feat", }, ], From 0878114a5d37a5c82793064088b185e9b12c8fb7 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 23 Jan 2020 15:57:18 +0800 Subject: [PATCH 07/35] refactor(commands/changelog): use jinja2 template instead of string concatenation to build changelog * install jinja2 * use named caputure group for changelog_pattern --- commitizen/commands/changelog.py | 78 +++++++++++-------- commitizen/cz/changelog_template.j2 | 13 ++++ .../conventional_commits.py | 6 +- pyproject.toml | 2 +- 4 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 commitizen/cz/changelog_template.j2 diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 54554c170c..dc3c131529 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -1,71 +1,81 @@ import re from collections import OrderedDict -from commitizen import factory, out, git +from jinja2 import Template + +from commitizen import factory, out, git, cz from commitizen.config import BaseConfig +try: + import importlib.resources as pkg_resources +except ImportError: + # Try backported to PY<37 `importlib_resources`. + import importlib_resources as pkg_resources + class Changelog: """Generate a changelog based on the commit history.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, args): self.config: BaseConfig = config self.cz = factory.commiter_factory(self.config) # TODO: make these attribute arguments - self.skip_merge = True - self.file_name = "CHANGELOG.md" + self.skip_merge = args["skip_merge"] + self.file_name = args["file_name"] + self.dry_run = args["dry_run"] def __call__(self): changelog_map = self.cz.changelog_map changelog_pattern = self.cz.changelog_pattern - if not changelog_map: + if not changelog_map or not changelog_pattern: out.error( f"'{self.config.settings['name']}' rule does not support changelog" ) pat = re.compile(changelog_pattern) - changelog_entry_key = "Unreleased" - changelog_entry_values = OrderedDict( - {value: [] for value in changelog_map.values()} - ) + entries = OrderedDict() + commits = git.get_commits() tag_map = {tag.rev: tag.name for tag in git.get_tags()} - changelog_str = "# Changelog\n" + # The latest commit is not tagged + latest_commit = commits[0] + if latest_commit.rev not in tag_map: + current_key = "Unreleased" + entries[current_key] = OrderedDict( + {value: [] for value in changelog_map.values()} + ) + else: + current_key = tag_map[latest_commit.rev] + for commit in commits: if self.skip_merge and commit.message.startswith("Merge"): continue if commit.rev in tag_map: - changelog_str += f"\n## {changelog_entry_key}\n" - for key, values in changelog_entry_values.items(): - if not values: - continue - changelog_str += f"* {key}\n" - for value in values: - changelog_str += f" * {value}\n" - changelog_entry_key = tag_map[commit.rev] - - for message in commit.message.split("\n"): - result = pat.search(message) - if not result: - continue - found_keyword = result.group(0) - processed_commit = self.cz.process_commit(commit.message) - changelog_entry_values[changelog_map[found_keyword]].append( - processed_commit + current_key = tag_map[commit.rev] + entries[current_key] = OrderedDict( + {value: [] for value in changelog_map.values()} ) - break - changelog_str += f"\n## {changelog_entry_key}\n" - for key, values in changelog_entry_values.items(): - if not values: + matches = pat.match(commit.message) + if not matches: continue - changelog_str += f"* {key}\n" - for value in values: - changelog_str += f" * {value}\n" + + processed_commit = self.cz.process_commit(commit.message) + for group_name, commit_type in changelog_map.items(): + if matches.group(group_name): + entries[current_key][commit_type].append(processed_commit) + break + + template_file = pkg_resources.read_text(cz, "changelog_template.j2") + jinja_template = Template(template_file) + changelog_str = jinja_template.render(entries=entries) + if self.dry_run: + out.write(changelog_str) + raise SystemExit(0) with open(self.file_name, "w") as changelog_file: changelog_file.write(changelog_str) diff --git a/commitizen/cz/changelog_template.j2 b/commitizen/cz/changelog_template.j2 new file mode 100644 index 0000000000..4d3516577e --- /dev/null +++ b/commitizen/cz/changelog_template.j2 @@ -0,0 +1,13 @@ +# CHANGELOG + +{% for entry_key, entry_value in entries.items()-%} +## {{entry_key}} +{% for type, commits in entry_value.items()-%} +{%-if commits-%} +### {{type}} +{% for commit in commits-%} +- {{commit}} +{%-endfor %} +{% endif %} +{%-endfor %} +{% endfor %} diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index c785977b0b..29c9d9a880 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -31,10 +31,10 @@ def parse_subject(text): class ConventionalCommitsCz(BaseCommitizen): bump_pattern = defaults.bump_pattern bump_map = defaults.bump_map - changelog_pattern = r"^(BREAKING CHANGE|feat|fix)" - changelog_map = OrderedDict( - {"BREAKING CHANGES": "breaking", "feat": "feat", "fix": "fix"} + changelog_pattern = ( + r"(?P.*\n\nBREAKING CHANGE)|(?P^feat)|(?P^fix)" ) + changelog_map = OrderedDict({"break": "breaking", "feat": "feat", "fix": "fix"}) def questions(self) -> List[Dict[str, Any]]: questions: List[Dict[str, Any]] = [ diff --git a/pyproject.toml b/pyproject.toml index 38f69fca9a..97add2bf9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ colorama = "^0.4.1" termcolor = "^1.1" packaging = ">=19,<21" tomlkit = "^0.5.3" -jinja2 = {version = "^2.10.3", optional = true} +jinja2 = "^2.10.3" [tool.poetry.dev-dependencies] ipython = "^7.2" From 4982faede0a4603c8a427dc5d111ee4a16320323 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 21:35:38 +0800 Subject: [PATCH 08/35] fix(cli): add changelog arguments --- commitizen/cli.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index 352e83f93f..d6b876c710 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -40,8 +40,27 @@ }, { "name": ["changelog", "ch"], - "help": "create new changelog", + "help": "generate changelog (note that it will overwrite existing file)", "func": commands.Changelog, + "arguments": [ + { + "name": "--dry-run", + "action": "store_true", + "default": False, + "help": "show changelog to stdout", + }, + { + "name": "--skip-merge", + "action": "store_true", + "default": False, + "help": "whether to skip merge commit", + }, + { + "name": "--file-name", + "default": "CHANGELOG.md", + "help": "file name of changelog", + }, + ], }, { "name": ["commit", "c"], From 2cfd9defe8f168dce373526c225b631e06523009 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 21:36:27 +0800 Subject: [PATCH 09/35] feat(commands/changlog): add --start-rev argument to `cz changelog` --- commitizen/cli.py | 8 ++++++++ commitizen/commands/changelog.py | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index d6b876c710..ce6106863a 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -60,6 +60,14 @@ "default": "CHANGELOG.md", "help": "file name of changelog", }, + { + "name": "--start-rev", + "default": None, + "help": ( + "start rev of the changelog." + "If not set, it will generate changelog from the start" + ), + }, ], }, { diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index dc3c131529..0f75138f07 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -20,10 +20,10 @@ def __init__(self, config: BaseConfig, args): self.config: BaseConfig = config self.cz = factory.commiter_factory(self.config) - # TODO: make these attribute arguments self.skip_merge = args["skip_merge"] self.file_name = args["file_name"] self.dry_run = args["dry_run"] + self.start_rev = args["start_rev"] def __call__(self): changelog_map = self.cz.changelog_map @@ -37,7 +37,10 @@ def __call__(self): entries = OrderedDict() - commits = git.get_commits() + if self.start_rev: + commits = git.get_commits(start=self.start_rev) + else: + commits = git.get_commits() tag_map = {tag.rev: tag.name for tag in git.get_tags()} # The latest commit is not tagged From 3df0a7ac831b7b4a8443e69264763899fcf2b3b2 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 21:48:52 +0800 Subject: [PATCH 10/35] style(cli): fix flake8 issue --- commitizen/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index ce6106863a..302ca7fb2a 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -40,7 +40,9 @@ }, { "name": ["changelog", "ch"], - "help": "generate changelog (note that it will overwrite existing file)", + "help": ( + "generate changelog (note that it will overwrite existing file)" + ), "func": commands.Changelog, "arguments": [ { From ace33cc14736123dad1f8329779610b569f32634 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 21:53:17 +0800 Subject: [PATCH 11/35] refactor(commands/changelog): remove redundant if statement --- commitizen/commands/changelog.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 0f75138f07..7d66d1e3c0 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -35,14 +35,10 @@ def __call__(self): pat = re.compile(changelog_pattern) - entries = OrderedDict() - - if self.start_rev: - commits = git.get_commits(start=self.start_rev) - else: - commits = git.get_commits() + commits = git.get_commits(start=self.start_rev) tag_map = {tag.rev: tag.name for tag in git.get_tags()} + entries = OrderedDict() # The latest commit is not tagged latest_commit = commits[0] if latest_commit.rev not in tag_map: From a9285253874e40987be06e05f1fa06ad2cc78c9e Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 22:18:28 +0800 Subject: [PATCH 12/35] refactor(tests/utils): move create_file_and_commit to tests/utils --- tests/commands/test_bump_command.py | 13 +------------ tests/utils.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 tests/utils.py diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index e5ef886f63..3907d19527 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -1,20 +1,9 @@ import sys -import uuid -from pathlib import Path -from typing import Optional import pytest from commitizen import cli, cmd, git - - -def create_file_and_commit(message: str, filename: Optional[str] = None): - if not filename: - filename = str(uuid.uuid4()) - - Path(f"./{filename}").touch() - cmd.run("git add .") - git.commit(message) +from tests.utils import create_file_and_commit @pytest.mark.usefixtures("tmp_commitizen_project") diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000000..64598b8df1 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,14 @@ +import uuid +from pathlib import Path +from typing import Optional + +from commitizen import cmd, git + + +def create_file_and_commit(message: str, filename: Optional[str] = None): + if not filename: + filename = str(uuid.uuid4()) + + Path(f"./{filename}").touch() + cmd.run("git add .") + git.commit(message) From 5f9cf46a0b284c41a5baaa61e49c33be21d785e3 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 22:24:45 +0800 Subject: [PATCH 13/35] feat(commands/changelog): exit when there is no commit exists --- commitizen/commands/changelog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 7d66d1e3c0..9fc97e7285 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -5,6 +5,7 @@ from commitizen import factory, out, git, cz from commitizen.config import BaseConfig +from commitizen.error_codes import NO_COMMITS_FOUND try: import importlib.resources as pkg_resources @@ -36,6 +37,9 @@ def __call__(self): pat = re.compile(changelog_pattern) commits = git.get_commits(start=self.start_rev) + if not commits: + raise SystemExit(NO_COMMITS_FOUND) + tag_map = {tag.rev: tag.name for tag in git.get_tags()} entries = OrderedDict() From 3a80c56ac831de5e6b58fb4544d5ae03bdcdf04e Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 22:42:25 +0800 Subject: [PATCH 14/35] fix(commands/changelog): remove --skip-merge argument by default, unrelated commits are ignored --- commitizen/cli.py | 6 ------ commitizen/commands/changelog.py | 4 ---- commitizen/cz/changelog_template.j2 | 12 ++++++------ 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index 302ca7fb2a..197fd177e3 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -51,12 +51,6 @@ "default": False, "help": "show changelog to stdout", }, - { - "name": "--skip-merge", - "action": "store_true", - "default": False, - "help": "whether to skip merge commit", - }, { "name": "--file-name", "default": "CHANGELOG.md", diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 9fc97e7285..5dd54663c3 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -21,7 +21,6 @@ def __init__(self, config: BaseConfig, args): self.config: BaseConfig = config self.cz = factory.commiter_factory(self.config) - self.skip_merge = args["skip_merge"] self.file_name = args["file_name"] self.dry_run = args["dry_run"] self.start_rev = args["start_rev"] @@ -54,9 +53,6 @@ def __call__(self): current_key = tag_map[latest_commit.rev] for commit in commits: - if self.skip_merge and commit.message.startswith("Merge"): - continue - if commit.rev in tag_map: current_key = tag_map[commit.rev] entries[current_key] = OrderedDict( diff --git a/commitizen/cz/changelog_template.j2 b/commitizen/cz/changelog_template.j2 index 4d3516577e..eb4a096cc4 100644 --- a/commitizen/cz/changelog_template.j2 +++ b/commitizen/cz/changelog_template.j2 @@ -1,13 +1,13 @@ # CHANGELOG -{% for entry_key, entry_value in entries.items()-%} +{% for entry_key, entry_value in entries.items() -%} ## {{entry_key}} -{% for type, commits in entry_value.items()-%} -{%-if commits-%} +{% for type, commits in entry_value.items() -%} +{%- if commits -%} ### {{type}} {% for commit in commits-%} - {{commit}} -{%-endfor %} +{%- endfor %} {% endif %} -{%-endfor %} -{% endfor %} +{%- endfor %} +{%- endfor %} From 2a2a29e6121cdeb52bdfeecf67852cb617e7d8e3 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 22:47:13 +0800 Subject: [PATCH 15/35] fix(commitizen/cz): set changelog_map, changelog_pattern to none as default without this default, getting these attributes will be an error --- commitizen/commands/changelog.py | 4 +++- commitizen/cz/base.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 5dd54663c3..56f93a922d 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -5,7 +5,7 @@ from commitizen import factory, out, git, cz from commitizen.config import BaseConfig -from commitizen.error_codes import NO_COMMITS_FOUND +from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP try: import importlib.resources as pkg_resources @@ -32,11 +32,13 @@ def __call__(self): out.error( f"'{self.config.settings['name']}' rule does not support changelog" ) + raise SystemExit(NO_PATTERN_MAP) pat = re.compile(changelog_pattern) commits = git.get_commits(start=self.start_rev) if not commits: + out.error("No commits found") raise SystemExit(NO_COMMITS_FOUND) tag_map = {tag.rev: tag.name for tag in git.get_tags()} diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index d41b876c58..ff6147d29f 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -9,6 +9,8 @@ class BaseCommitizen(metaclass=ABCMeta): bump_pattern: Optional[str] = None bump_map: Optional[dict] = None + changelog_pattern: Optional[str] = None + changelog_map: Optional[dict] = None default_style_config: List[Tuple[str, str]] = [ ("qmark", "fg:#ff9d00 bold"), ("question", "bold"), From 0b0676d11ed7036c44d97972d669ad60b8136678 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 23 Jan 2020 23:17:15 +0800 Subject: [PATCH 16/35] fix(changelog_template): fix list format --- commitizen/cz/changelog_template.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitizen/cz/changelog_template.j2 b/commitizen/cz/changelog_template.j2 index eb4a096cc4..d3fba14668 100644 --- a/commitizen/cz/changelog_template.j2 +++ b/commitizen/cz/changelog_template.j2 @@ -7,7 +7,7 @@ ### {{type}} {% for commit in commits-%} - {{commit}} -{%- endfor %} +{% endfor %} {% endif %} {%- endfor %} {%- endfor %} From 2b15a1bfdab50888ec07a530bf04ca9b13387ad0 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 23 Jan 2020 23:17:47 +0800 Subject: [PATCH 17/35] test(commands/changelog): add test case for changelog command --- tests/commands/test_changelog_command.py | 65 ++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/commands/test_changelog_command.py diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py new file mode 100644 index 0000000000..d6bf680553 --- /dev/null +++ b/tests/commands/test_changelog_command.py @@ -0,0 +1,65 @@ +import sys + +import pytest + +from commitizen import cli +from tests.utils import create_file_and_commit + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_on_empty_project(mocker): + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(SystemExit): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_from_start(mocker, capsys): + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: not in changelog") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + assert out == "# CHANGELOG\n\n## Unreleased\n### feat\n- new file\n\n\n" + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_from_version_zero_point_two(mocker, capsys): + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: not in changelog") + + # create tag + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + capsys.readouterr() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: after 0.2") + + testargs = ["cz", "changelog", "--start-rev", "0.2.0", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + assert out == "# CHANGELOG\n\n## Unreleased\n### feat\n- after 0.2\n- after 0.2.0\n\n\n" + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_with_unsupported_cz(mocker, capsys): + testargs = ["cz", "-n", "cz_jira", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(SystemExit): + cli.main() + out, err = capsys.readouterr() + assert "'cz_jira' rule does not support changelog" in err From a032fa4926e2e437b3096d604fe2008e59a25ec0 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 23 Jan 2020 23:40:20 +0800 Subject: [PATCH 18/35] refactor(templates): move changelog_template from cz to templates --- commitizen/commands/changelog.py | 13 +++++-------- commitizen/templates/__init__.py | 0 commitizen/{cz => templates}/changelog_template.j2 | 0 3 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 commitizen/templates/__init__.py rename commitizen/{cz => templates}/changelog_template.j2 (100%) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 56f93a922d..8ea3c5cda5 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -1,18 +1,13 @@ import re +import pkg_resources from collections import OrderedDict from jinja2 import Template -from commitizen import factory, out, git, cz +from commitizen import factory, out, git from commitizen.config import BaseConfig from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP -try: - import importlib.resources as pkg_resources -except ImportError: - # Try backported to PY<37 `importlib_resources`. - import importlib_resources as pkg_resources - class Changelog: """Generate a changelog based on the commit history.""" @@ -71,7 +66,9 @@ def __call__(self): entries[current_key][commit_type].append(processed_commit) break - template_file = pkg_resources.read_text(cz, "changelog_template.j2") + template_file = pkg_resources.resource_string( + __name__, "../templates/changelog_template.j2" + ).decode("utf-8") jinja_template = Template(template_file) changelog_str = jinja_template.render(entries=entries) if self.dry_run: diff --git a/commitizen/templates/__init__.py b/commitizen/templates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/commitizen/cz/changelog_template.j2 b/commitizen/templates/changelog_template.j2 similarity index 100% rename from commitizen/cz/changelog_template.j2 rename to commitizen/templates/changelog_template.j2 From 384018d0bf386947bd82f2f8d9d7f3ae65517ac4 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 08:08:12 +0800 Subject: [PATCH 19/35] refactor(cli): reorder commands --- commitizen/cli.py | 100 +++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index 197fd177e3..b0cb76f8fd 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -34,37 +34,9 @@ # "required": True, "commands": [ { - "name": "ls", - "help": "show available commitizens", - "func": commands.ListCz, - }, - { - "name": ["changelog", "ch"], - "help": ( - "generate changelog (note that it will overwrite existing file)" - ), - "func": commands.Changelog, - "arguments": [ - { - "name": "--dry-run", - "action": "store_true", - "default": False, - "help": "show changelog to stdout", - }, - { - "name": "--file-name", - "default": "CHANGELOG.md", - "help": "file name of changelog", - }, - { - "name": "--start-rev", - "default": None, - "help": ( - "start rev of the changelog." - "If not set, it will generate changelog from the start" - ), - }, - ], + "name": ["init"], + "help": "init commitizen configuration", + "func": commands.Init, }, { "name": ["commit", "c"], @@ -83,6 +55,11 @@ }, ], }, + { + "name": "ls", + "help": "show available commitizens", + "func": commands.ListCz, + }, { "name": "example", "help": "show commit example", @@ -142,33 +119,30 @@ ], }, { - "name": ["version"], + "name": ["changelog", "ch"], "help": ( - "get the version of the installed commitizen or the current project" - " (default: installed commitizen)" + "generate changelog (note that it will overwrite existing file)" ), - "func": commands.Version, + "func": commands.Changelog, "arguments": [ { - "name": ["-p", "--project"], - "help": "get the version of the current project", + "name": "--dry-run", "action": "store_true", - "exclusive_group": "group1", + "default": False, + "help": "show changelog to stdout", }, { - "name": ["-c", "--commitizen"], - "help": "get the version of the installed commitizen", - "action": "store_true", - "exclusive_group": "group1", + "name": "--file-name", + "default": "CHANGELOG.md", + "help": "file name of changelog", }, { - "name": ["-v", "--verbose"], + "name": "--start-rev", + "default": None, "help": ( - "get the version of both the installed commitizen " - "and the current project" + "start rev of the changelog." + "If not set, it will generate changelog from the start" ), - "action": "store_true", - "exclusive_group": "group1", }, ], }, @@ -194,9 +168,35 @@ ], }, { - "name": ["init"], - "help": "init commitizen configuration", - "func": commands.Init, + "name": ["version"], + "help": ( + "get the version of the installed commitizen or the current project" + " (default: installed commitizen)" + ), + "func": commands.Version, + "arguments": [ + { + "name": ["-p", "--project"], + "help": "get the version of the current project", + "action": "store_true", + "exclusive_group": "group1", + }, + { + "name": ["-c", "--commitizen"], + "help": "get the version of the installed commitizen", + "action": "store_true", + "exclusive_group": "group1", + }, + { + "name": ["-v", "--verbose"], + "help": ( + "get the version of both the installed commitizen " + "and the current project" + ), + "action": "store_true", + "exclusive_group": "group1", + }, + ], }, ], }, From 5ae9058fd5762297e40cb84711923f1f0fb98f5a Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 11:37:16 +0800 Subject: [PATCH 20/35] docs(README): add changelog command --- docs/README.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/README.md b/docs/README.md index c3f94ccf64..f05e03929b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -77,29 +77,34 @@ cz c ```bash $ cz --help usage: cz [-h] [--debug] [-n NAME] [--version] - {ls,commit,c,example,info,schema,bump} ... + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... Commitizen is a cli tool to generate conventional commits. For more information about the topic go to https://conventionalcommits.org/ optional arguments: --h, --help show this help message and exit ---debug use debug mode --n NAME, --name NAME use the given commitizen ---version get the version of the installed commitizen + -h, --help show this help message and exit + --debug use debug mode + -n NAME, --name NAME use the given commitizen (default: + cz_conventional_commits) + --version get the version of the installed commitizen commands: -{ls,commit,c,example,info,schema,bump} - ls show available commitizens + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + init init commitizen configuration commit (c) create new commit + ls show available commitizens example show commit example info show information about the cz schema show commit schema bump bump semantic version based on the git log + changelog (ch) generate changelog (note that it will overwrite + existing file) + check validates that a commit message matches the commitizen + schema version get the version of the installed commitizen or the current project (default: installed commitizen) - check validates that a commit message matches the commitizen schema - init init commitizen configuration ``` ## FAQ From 1ece991a2f6f8dc85d3c1364853854300a5bb825 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 11:38:22 +0800 Subject: [PATCH 21/35] refactor(templates): remove unneeded __init__ file --- commitizen/templates/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 commitizen/templates/__init__.py diff --git a/commitizen/templates/__init__.py b/commitizen/templates/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 5b9c564a25e8c14c495478a14e5956497da3998f Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 11:38:46 +0800 Subject: [PATCH 22/35] style(tests/commands/changelog): blackify --- tests/commands/test_changelog_command.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index d6bf680553..a053e6c238 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -51,7 +51,10 @@ def test_changlog_from_version_zero_point_two(mocker, capsys): cli.main() out, _ = capsys.readouterr() - assert out == "# CHANGELOG\n\n## Unreleased\n### feat\n- after 0.2\n- after 0.2.0\n\n\n" + assert ( + out + == "# CHANGELOG\n\n## Unreleased\n### feat\n- after 0.2\n- after 0.2.0\n\n\n" + ) @pytest.mark.usefixtures("tmp_commitizen_project") From 11e24d32e22a9ccc82d8a6b17b9b81add441c8f0 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 11:39:54 +0800 Subject: [PATCH 23/35] refactor(templates): rename as "keep_a_changelog_template.j2" --- commitizen/commands/changelog.py | 2 +- .../{changelog_template.j2 => keep_a_changelog_template.j2} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename commitizen/templates/{changelog_template.j2 => keep_a_changelog_template.j2} (100%) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 8ea3c5cda5..24926192c8 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -67,7 +67,7 @@ def __call__(self): break template_file = pkg_resources.resource_string( - __name__, "../templates/changelog_template.j2" + __name__, "../templates/keep_a_changelog_template.j2" ).decode("utf-8") jinja_template = Template(template_file) changelog_str = jinja_template.render(entries=entries) diff --git a/commitizen/templates/changelog_template.j2 b/commitizen/templates/keep_a_changelog_template.j2 similarity index 100% rename from commitizen/templates/changelog_template.j2 rename to commitizen/templates/keep_a_changelog_template.j2 From 2becf57dbe2ef079b053b721dc96ef48a06d0759 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 16:36:17 +0800 Subject: [PATCH 24/35] feat(commands/changelog): make changelog_file an option in config --- commitizen/cli.py | 3 +-- commitizen/commands/changelog.py | 2 +- commitizen/defaults.py | 1 + tests/test_conf.py | 3 +++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index b0cb76f8fd..9d5130dce9 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -133,8 +133,7 @@ }, { "name": "--file-name", - "default": "CHANGELOG.md", - "help": "file name of changelog", + "help": "file name of changelog (default: 'CHANGELOG.md')", }, { "name": "--start-rev", diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 24926192c8..280f1b8f8c 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -16,7 +16,7 @@ def __init__(self, config: BaseConfig, args): self.config: BaseConfig = config self.cz = factory.commiter_factory(self.config) - self.file_name = args["file_name"] + self.file_name = args["file_name"] or self.config.settings.get("changelog_file") self.dry_run = args["dry_run"] self.start_rev = args["start_rev"] diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 477a48e4a1..a694a11840 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -13,6 +13,7 @@ "version_files": [], "tag_format": None, # example v$version "bump_message": None, # bumped v$current_version to $new_version + "changelog_file": "CHANGELOG.md", } MAJOR = "MAJOR" diff --git a/tests/test_conf.py b/tests/test_conf.py index 58580e52a6..00dc4d40ad 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -44,6 +44,7 @@ "bump_message": None, "version_files": ["commitizen/__version__.py", "pyproject.toml"], "style": [["pointer", "reverse"], ["question", "underline"]], + "changelog_file": "CHANGELOG.md", } _new_settings = { @@ -53,6 +54,7 @@ "bump_message": None, "version_files": ["commitizen/__version__.py", "pyproject.toml"], "style": [["pointer", "reverse"], ["question", "underline"]], + "changelog_file": "CHANGELOG.md", } _read_settings = { @@ -60,6 +62,7 @@ "version": "1.0.0", "version_files": ["commitizen/__version__.py", "pyproject.toml"], "style": [["pointer", "reverse"], ["question", "underline"]], + "changelog_file": "CHANGELOG.md", } From 81b27c575e778da72731d8dbe6d219c0772c378c Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 16:38:08 +0800 Subject: [PATCH 25/35] docs(config): add changlog_file a config option --- docs/config.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/config.md b/docs/config.md index d34c228ff9..67ce1f9a4f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -67,8 +67,9 @@ The extra tab before the square brackets (`]`) at the end is required. | -------- | ---- | ------- | ----------- | | `name` | `str` | `"cz_conventional_commits"` | Name of the committing rules to use | | `version` | `str` | `None` | Current version. Example: "0.1.2" | -| `version_files` | `list` | `[ ]` | Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [See more](https://commitizen-tools.github.io/commitizen/bump#files) | -| `tag_format` | `str` | `None` | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more](https://commitizen-tools.github.io/commitizen/bump#tag_format) | -| `bump_message` | `str` | `None` | Create custom commit message, useful to skip ci. [See more](https://commitizen-tools.github.io/commitizen/bump#bump_message) | +| `version_files` | `list` | `[ ]` | Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [See more](https://woile.github.io/commitizen/bump#files) | +| `tag_format` | `str` | `None` | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more](https://woile.github.io/commitizen/bump#tag_format) | +| `bump_message` | `str` | `None` | Create custom commit message, useful to skip ci. [See more](https://woile.github.io/commitizen/bump#bump_message) | +| `changelog_file` | `str` | `CHANGELOG.md` | filename of exported changelog | | `style` | `list` | see above | Style for the prompts (It will merge this value with default style.) [See More (Styling your prompts with your favorite colors)](https://github.com/tmbo/questionary#additional-features) | | `customize` | `dict` | `None` | **This is only supported when config through `toml`.** Custom rules for committing and bumping. [See more](https://commitizen-tools.github.io/commitizen/customization/) | From 78b321e9999972e8aacce9fbf4f7f587d06f7b01 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Mon, 16 Mar 2020 22:25:18 +0800 Subject: [PATCH 26/35] fix(cz/conventional_commits): fix schema_pattern break due to rebase --- commitizen/cz/conventional_commits/conventional_commits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index 29c9d9a880..8026532779 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -179,7 +179,7 @@ def schema(self) -> str: def schema_pattern(self) -> str: PATTERN = ( r"(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)" - r"(\(\S+\))?:\s.*" + r"(\(\S+\))?:(\s.*)" ) return PATTERN From 5bf5542e78ea575eebf258f3ac3cc9eb3a15a03f Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Mon, 16 Mar 2020 22:25:44 +0800 Subject: [PATCH 27/35] style: reformat --- commitizen/changelog.py | 2 +- commitizen/commands/__init__.py | 4 +--- commitizen/commands/changelog.py | 4 ++-- tests/test_changelog.py | 2 -- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 495a4513c4..b902640897 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -18,8 +18,8 @@ Options: - Generate full or partial changelog """ -from typing import Generator, List, Dict, Iterable import re +from typing import Dict, Generator, Iterable, List MD_VERSION_RE = r"^##\s(?P[a-zA-Z0-9.+]+)\s?\(?(?P[0-9-]+)?\)?" MD_CATEGORY_RE = r"^###\s(?P[a-zA-Z0-9.+\s]+)" diff --git a/commitizen/commands/__init__.py b/commitizen/commands/__init__.py index fde589f8df..806e384522 100644 --- a/commitizen/commands/__init__.py +++ b/commitizen/commands/__init__.py @@ -1,4 +1,5 @@ from .bump import Bump +from .changelog import Changelog from .check import Check from .commit import Commit from .example import Example @@ -7,9 +8,6 @@ from .list_cz import ListCz from .schema import Schema from .version import Version -from .init import Init -from .changelog import Changelog - __all__ = ( "Bump", diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 280f1b8f8c..b9bd966d27 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -1,10 +1,10 @@ import re -import pkg_resources from collections import OrderedDict +import pkg_resources from jinja2 import Template -from commitizen import factory, out, git +from commitizen import factory, git, out from commitizen.config import BaseConfig from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP diff --git a/tests/test_changelog.py b/tests/test_changelog.py index ba0a730859..dd34b9be92 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,11 +1,9 @@ import os - import pytest from commitizen import changelog - COMMIT_LOG = [ "bump: version 1.5.0 → 1.5.1", "", From 65d82683f6ef8fd90659c549860c13a01582e72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Sun, 22 Mar 2020 21:35:08 +0100 Subject: [PATCH 28/35] refactor(changelog): rename category to change_type to fit 'keep a changelog' --- commitizen/changelog.py | 26 +++++++-------- .../conventional_commits.py | 2 ++ tests/test_changelog.py | 32 +++++++++---------- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index b902640897..b5f29f2f98 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -22,10 +22,10 @@ from typing import Dict, Generator, Iterable, List MD_VERSION_RE = r"^##\s(?P[a-zA-Z0-9.+]+)\s?\(?(?P[0-9-]+)?\)?" -MD_CATEGORY_RE = r"^###\s(?P[a-zA-Z0-9.+\s]+)" +MD_CHANGE_TYPE_RE = r"^###\s(?P[a-zA-Z0-9.+\s]+)" MD_MESSAGE_RE = r"^-\s(\*{2}(?P[a-zA-Z0-9]+)\*{2}:\s)?(?P.+)" md_version_c = re.compile(MD_VERSION_RE) -md_category_c = re.compile(MD_CATEGORY_RE) +md_change_type_c = re.compile(MD_CHANGE_TYPE_RE) md_message_c = re.compile(MD_MESSAGE_RE) @@ -83,8 +83,8 @@ def parse_md_version(md_version: str) -> Dict: return m.groupdict() -def parse_md_category(md_category: str) -> Dict: - m = md_category_c.match(md_category) +def parse_md_change_type(md_change_type: str) -> Dict: + m = md_change_type_c.match(md_change_type) if not m: return {} return m.groupdict() @@ -97,31 +97,31 @@ def parse_md_message(md_message: str) -> Dict: return m.groupdict() -def transform_category(category: str) -> str: - _category_lower = category.lower() +def transform_change_type(change_type: str) -> str: + _change_type_lower = change_type.lower() for match_value, output in CATEGORIES: - if re.search(match_value, _category_lower): + if re.search(match_value, _change_type_lower): return output else: - raise ValueError(f"Could not match a category with {category}") + raise ValueError(f"Could not match a change_type with {change_type}") def generate_block_tree(block: List[str]) -> Dict: tree: Dict = {"commits": []} - category = None + change_type = None for line in block: if line.startswith("## "): - category = None + change_type = None tree = {**tree, **parse_md_version(line)} elif line.startswith("### "): - result = parse_md_category(line) + result = parse_md_change_type(line) if not result: continue - category = transform_category(result.get("category", "")) + change_type = transform_change_type(result.get("change_type", "")) elif line.startswith("- "): commit = parse_md_message(line) - commit["category"] = category + commit["change_type"] = change_type tree["commits"].append(commit) else: print("it's something else: ", line) diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index 8026532779..8dc4cec623 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -193,4 +193,6 @@ def info(self) -> str: def process_commit(self, commit: str) -> str: pat = re.compile(self.schema_pattern()) m = re.match(pat, commit) + if m is None: + return '' return m.group(3).strip() diff --git a/tests/test_changelog.py b/tests/test_changelog.py index dd34b9be92..8052832f25 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -102,8 +102,8 @@ def test_read_changelog_blocks(existing_changelog_file): CATEGORIES_CASES: list = [ ("## 1.0.0 (2019-07-12)", {}), ("## 2.3.0a0", {}), - ("### Bug fixes", {"category": "Bug fixes"}), - ("### Features", {"category": "Features"}), + ("### Bug fixes", {"change_type": "Bug fixes"}), + ("### Features", {"change_type": "Features"}), ("- issue in poetry add preventing the installation in py36", {}), ] CATEGORIES_TRANSFORMATIONS: list = [ @@ -133,13 +133,13 @@ def test_parse_md_version(test_input, expected): @pytest.mark.parametrize("test_input,expected", CATEGORIES_CASES) -def test_parse_md_category(test_input, expected): - assert changelog.parse_md_category(test_input) == expected +def test_parse_md_change_type(test_input, expected): + assert changelog.parse_md_change_type(test_input) == expected @pytest.mark.parametrize("test_input,expected", CATEGORIES_TRANSFORMATIONS) -def test_transform_category(test_input, expected): - assert changelog.transform_category(test_input) == expected +def test_transform_change_type(test_input, expected): + assert changelog.transform_change_type(test_input) == expected @pytest.mark.parametrize("test_input,expected", MESSAGES_CASES) @@ -147,10 +147,10 @@ def test_parse_md_message(test_input, expected): assert changelog.parse_md_message(test_input) == expected -def test_transform_category_fail(): +def test_transform_change_type_fail(): with pytest.raises(ValueError) as excinfo: - changelog.transform_category("Bugs") - assert "Could not match a category" in str(excinfo.value) + changelog.transform_change_type("Bugs") + assert "Could not match a change_type" in str(excinfo.value) def test_generate_block_tree(existing_changelog_file): @@ -162,16 +162,16 @@ def test_generate_block_tree(existing_changelog_file): { "scope": None, "message": "issue in poetry add preventing the installation in py36", - "category": "fix", + "change_type": "fix", }, - {"scope": "users", "message": "lorem ipsum apap", "category": "fix"}, + {"scope": "users", "message": "lorem ipsum apap", "change_type": "fix"}, { "scope": None, "message": ( "it is possible to specify a pattern to be matched " "in configuration files bump." ), - "category": "feat", + "change_type": "feat", }, ], "version": "1.0.0", @@ -191,23 +191,23 @@ def test_generate_full_tree(existing_changelog_file): "message": ( "issue in poetry add preventing the installation in py36" ), - "category": "fix", + "change_type": "fix", }, - {"scope": "users", "message": "lorem ipsum apap", "category": "fix"}, + {"scope": "users", "message": "lorem ipsum apap", "change_type": "fix"}, { "scope": None, "message": ( "it is possible to specify a pattern to be matched " "in configuration files bump." ), - "category": "feat", + "change_type": "feat", }, ], "version": "1.0.0", "date": "2019-07-12", }, { - "commits": [{"scope": None, "message": "holis", "category": "fix"}], + "commits": [{"scope": None, "message": "holis", "change_type": "fix"}], "version": "0.9", "date": "2019-07-11", }, From e0a1b49a30681b635ce8f5d559136bcaf81d18c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= Date: Sun, 19 Apr 2020 22:33:52 +0200 Subject: [PATCH 29/35] refactor(changelog): use functions from changelog.py --- CHANGELOG.md | 503 ++++++++-- commitizen/changelog.py | 189 ++-- commitizen/changelog_parser.py | 133 +++ commitizen/cli.py | 9 + commitizen/commands/changelog.py | 96 +- commitizen/cz/base.py | 1 + .../conventional_commits.py | 9 +- commitizen/defaults.py | 2 + commitizen/git.py | 15 +- .../templates/keep_a_changelog_template.j2 | 30 +- docs/changelog.md | 49 + pyproject.toml | 1 + tests/CHANGELOG_FOR_TEST.md | 129 +++ tests/commands/test_changelog_command.py | 11 +- tests/test_changelog.py | 865 ++++++++++++++---- tests/test_changelog_parser.py | 194 ++++ 16 files changed, 1816 insertions(+), 420 deletions(-) create mode 100644 commitizen/changelog_parser.py create mode 100644 docs/changelog.md create mode 100644 tests/CHANGELOG_FOR_TEST.md create mode 100644 tests/test_changelog_parser.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d7d614abaf..b8736827ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,139 +1,504 @@ -# CHANGELOG -## v1.12.0 -### Feature +## Unreleased + +### Refactor + +- **changelog**: rename category to change_type to fit 'keep a changelog' +- **templates**: rename as "keep_a_changelog_template.j2" +- **templates**: remove unneeded __init__ file +- **cli**: reorder commands +- **templates**: move changelog_template from cz to templates +- **tests/utils**: move create_file_and_commit to tests/utils +- **commands/changelog**: remove redundant if statement +- **commands/changelog**: use jinja2 template instead of string concatenation to build changelog + +### Fix + +- **cz/conventional_commits**: fix schema_pattern break due to rebase +- **changelog_template**: fix list format +- **commitizen/cz**: set changelog_map, changelog_pattern to none as default +- **commands/changelog**: remove --skip-merge argument +- **cli**: add changelog arguments + +### Feat + +- **commands/changelog**: make changelog_file an option in config +- **commands/changelog**: exit when there is no commit exists +- **commands/changlog**: add --start-rev argument to `cz changelog` +- **changelog**: generate changelog based on git log +- **commands/changelog**: generate changelog_tree from all past commits +- **cz/conventinal_commits**: add changelog_map, changelog_pattern and implement process_commit +- **cz/base**: add default process_commit for processing commit message +- **changelog**: changelog tree generation from markdown + +## v1.17.0 (2020-03-15) + +### Refactor + +- **tests/bump**: use parameterize to group similliar tests +- **cz/connventional_commit**: use \S to check scope +- **git**: remove unnecessary dot between git range + +### Fix + +- **bump**: fix bump find_increment error + +### Feat + +- **commands/check**: add --rev-range argument for checking commits within some range + +## v1.16.4 (2020-03-03) + +### Fix + +- **commands/init**: fix clean up file when initialize commitizen config + +### Refactor + +- **defaults**: split config files into long term support and deprecated ones + +## v1.16.3 (2020-02-20) + +### Fix + +- replace README.rst with docs/README.md in config files + +### Refactor + +- **docs**: remove README.rst and use docs/README.md + +## v1.16.2 (2020-02-01) + +### Fix + +- **commands/check**: add bump into valid commit message of convention commit pattern + +## v1.16.1 (2020-02-01) + +### Fix + +- **pre-commit**: set pre-commit check stage to commit-msg + +## v1.16.0 (2020-01-21) + +### Refactor + +- **commands/bump**: rename parameter into bump_setting to distinguish bump_setting and argument +- **git**: rename get tag function to distinguish return str and GitTag +- **cmd**: reimplement how cmd is run +- **git**: Use GitCommit, GitTag object to store commit and git information +- **git**: make arguments other then start and end in get_commit keyword arguments +- **git**: Change get_commits into returning commits instead of lines of messsages + +### Feat + +- **git**: get_commits default from first_commit + +## v1.15.1 (2020-01-20) + +## v1.15.0 (2020-01-20) + +### Refactor + +- **tests/commands/bump**: use tmp_dir to replace self implemented tmp dir behavior +- **test_bump_command**: rename camel case variables +- **tests/commands/check**: use pytest fixture tmpdir replace self implemented contextmanager +- **test/commands/other**: replace unit test style mock with mocker fixture +- **tests/commands**: separate command unit tests into modules +- **tests/commands**: make commands related tests a module +- **git**: make find_git_project_root return None if it's not a git project +- **config/base_config**: make set_key not implemented +- **error_codes**: move all the error_codes to a module +- **config**: replace string type path with pathlib.Path + +### Fix + +- **cli**: fix --version not functional +- **git**: remove breakline in the return value of find_git_project_root + +### Feat + +- **config**: look up configuration in git project root +- **git**: add find_git_project_root + +## v1.14.2 (2020-01-14) + +### Fix + +- **github_workflow/pythonpublish**: use peaceiris/actions-gh-pages@v2 to publish docs + +## v1.14.1 (2020-01-11) + +## v1.14.0 (2020-01-06) + +### Refactor + +- **pre-commit-hooks**: add metadata for the check hook + +### Feat + +- **pre-commit-hooks**: add pre-commit hook + +### Fix + +- **cli**: fix the way default handled for name argument +- **cli**: fix name cannot be overwritten through config in newly refactored config design + +## v1.13.1 (2019-12-31) + +### Fix + +- **github_workflow/pythonpackage**: set git config for unit testing +- **scripts/test**: ensure the script fails once the first failure happens + +## v1.13.0 (2019-12-30) + +### Feat + +- add project version to command init + +## v1.12.0 (2019-12-30) + +### Feat - new init command -## v1.11.0 +## v1.10.3 (2019-12-29) -Ignore this version +### Refactor -## v1.10.0 +- **commands/bump**: use "version_files" internally +- **config**: set "files" to alias of "version_files" -### Feature +## v1.10.2 (2019-12-27) -- new argument `--files-only` in bump +### Refactor -## v1.9.2 +- new config system where each config type has its own class +- **config**: add type annotation to config property +- **config**: fix wrongly type annoated functions +- **config/ini_config**: move deprecation warning into class initialization +- **config**: use add_path instead of directly assigning _path +- **all**: replace all the _settings invoke with settings.update +- **cz/customize**: remove unnecessary statement "raise NotImplementedError("Not Implemented yet")" +- **config**: move default settings back to defaults +- **config**: Make config a class and each type of config (e.g., toml, ini) a child class ### Fix -- `--commit-msg-file` is now a required argument +- **config**: handle empty config file +- **config**: fix load global_conf even if it doesn't exist +- **config/ini_config**: replase outdated _parse_ini_settings with _parse_settings -## v1.9.1 +## v1.10.1 (2019-12-10) ### Fix -- exception `AnswerRequiredException` not caught +- **cli**: overwrite "name" only when it's not given +- **config**: fix typo -## v1.9.0 +## v1.10.0 (2019-11-28) -### Feature +### Feat -- new `version` command. `--version` will be deprecated in `2.0.0` -- new `git-cz` entrypoint. After installing `commitizen` you can run `git cz c` (#60) -- new `--dry-run` argument in `commit` (#56) -- new `cz check` command which checks if the message is valid with the rules (#59). Useful for git hooks. -- create a commiting rule directly in the config file (#54) -- support for multi-line body (#6) -- support for jinja templates. Install doign `pip install -U commitizen[jinja2]`. -- support for `.cz.toml`. The confs depending on `ConfigParser` will be deprecated in `2.0.0`. +- support for different commitizens in `cz check` +- **bump**: new argument --files-only +## v1.9.2 (2019-11-23) ### Fix -- tests were fixed -- windows error when removing folders (#67) -- typos in docs +- **commands/check.py**: --commit-msg-file is now a required argument -### Docs -- tutorial for gitlab ci -- tutorial for github actions +## v1.9.1 (2019-11-23) -## v1.8.0 +### Fix -### Feature +- **cz/exceptions**: exception AnswerRequiredException not caught (#89) -- new custom exception for commitizen -- commit is aborted if nothing in staging +## v1.9.0 (2019-11-22) -## v1.7.0 +### Feat -### Feature +- **Commands/check**: enforce the project to always use conventional commits +- **config**: add deprecation warning for loading config from ini files +- **cz/customize**: add jinja support to enhance template flexibility +- **cz/filters**: add required_validator and multiple_line_breaker +- **Commands/commit**: add ´--dry-run´ flag to the Commit command +- **cz/cz_customize**: implement info to support info and info_path +- **cz/cz_customize**: enable bump_pattern bump_map customization +- **cz/cz_customize**: implement customizable cz +- new 'git-cz' entrypoint -- new styles for the prompt -- new configuration option for the prompt styles +### Refactor -## v1.6.0 +- **config**: remove has_pyproject which is no longer used +- **cz/customize**: make jinja2 a custom requirement. if not installed use string.Tempalte instead +- **cz/utils**: rename filters as utils +- **cli**: add back --version and remove subcommand required constraint -### Feature +### Fix -- new retry argument to execute previous commit again +- commit dry-run doesnt require staging to be clean +- correct typo to spell "convention" +- removing folder in windows throwing a PermissionError +- **scripts**: add back the delelte poetry prefix +- **test_cli**: testing the version command -## v1.5.1 +## v1.8.0 (2019-11-12) ### Fix -- issue in poetry add preventing the installation in py36 +- **commands/commit**: catch exception raised by customization cz +- **cli**: handle the exception that command is not given +- **cli**: enforce subcommand as required + +### Refactor + +- **cz/conventional_commit**: make NoSubjectException inherit CzException and add error message +- **command/version**: use out.write instead of out.line +- **command**: make version a command instead of an argument + +### Feat + +- **cz**: add a base exception for cz customization +- **commands/commit**: abort commit if there is nothing to commit +- **git**: add is_staging_clean to check if there is any file in git staging + +## v1.7.0 (2019-11-08) + +### Fix + +- **cz**: fix bug in BaseCommitizen.style +- **cz**: fix merge_style usage error +- **cz**: remove breakpoint + +### Refactor + +- **cz**: change the color of default style + +### Feat + +- **config**: update style instead of overwrite +- **config**: parse style in config +- **commit**: make style configurable for commit command -## v1.5.0 +## v1.6.0 (2019-11-05) -### Feature +### Feat -- it is possible to specify a pattern to be matched in configuration `files` when doing bump. +- **commit**: new retry argument to execute previous commit again -## v1.4.0 +## v1.5.1 (2019-06-04) -### Feature +### Fix + +- #28 allows poetry add on py36 envs + +## v1.5.0 (2019-05-11) -- new argument (--yes) in bump to accept prompt questions +### Feat + +- **bump**: it is now possible to specify a pattern in the files attr to replace the version + +## v1.4.0 (2019-04-26) ### Fix -- error is shown when commiting fails. +- **bump**: handle commit and create tag failure + +### Feat -## v1.3.0 +- added argument yes to bump in order to accept questions -### Feature +## v1.3.0 (2019-04-24) -- bump: new commit message template, useful when having to skip ci. +### Feat -## v1.2.1 +- **bump**: new commit message template + +## v1.2.1 (2019-04-21) ### Fix -- prefixes like docs, build, etc no longer generate a PATCH +- **bump**: prefixes like docs, build, etc no longer generate a PATCH -## v1.2.0 +## v1.2.0 (2019-04-19) -### Feature +### Feat - custom cz plugins now support bumping version -## v1.1.1 +## v1.1.1 (2019-04-18) + +### Refactor + +- changed stdout statements +- **schema**: command logic removed from commitizen base +- **info**: command logic removed from commitizen base +- **example**: command logic removed from commitizen base +- **commit**: moved most of the commit logic to the commit command + +### 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 + +- new working bump command +- create version tag +- update given files with new version +- **config**: new set key, used to set version to cfg +- support for pyproject.toml +- first semantic version bump implementaiton ### Fix -- breaking change is now part of the body, instead of being in the subject +- removed all from commit +- fix config file not working + +### Refactor + +- added commands folder, better integration with decli + +## v1.0.0 (2019-03-01) + +### Refactor + +- removed delegator, added decli and many tests + +## 1.0.0b2 (2019-01-18) + +## v1.0.0b1 (2019-01-17) + +### Feat + +- py3 only, tests and conventional commits 1.0 + +## v0.9.11 (2018-12-17) + +### Fix + +- **config**: load config reads in order without failing if there is no commitizen section + +## v0.9.10 (2018-09-22) + +### Fix + +- parse scope (this is my punishment for not having tests) + +## v0.9.9 (2018-09-22) + +### Fix + +- parse scope empty + +## v0.9.8 (2018-09-22) + +### Fix + +- **scope**: parse correctly again + +## v0.9.7 (2018-09-22) + +### Fix + +- **scope**: parse correctly + +## v0.9.6 (2018-09-19) + +### Refactor + +- **conventionalCommit**: moved fitlers to questions instead of message + +### Fix + +- **manifest**: inluded missing files + +## v0.9.5 (2018-08-24) + +### Fix + +- **config**: home path for python versions between 3.0 and 3.5 + +## v0.9.4 (2018-08-02) + +### Feat + +- **cli**: added version + +## v0.9.3 (2018-07-28) + +### Feat + +- **commiter**: conventional commit is a bit more intelligent now + +## v0.9.2 (2017-11-11) + +### Refactor + +- **renamed conventional_changelog to conventional_commits, not backward compatible**: + +## v0.9.1 (2017-11-11) + +### Fix + +- **setup.py**: future is now required for every python version + +## v0.9.0 (2017-11-08) + +### Refactor + +- python 2 support + +## v0.8.6 (2017-11-08) + +## v0.8.5 (2017-11-08) + +## v0.8.4 (2017-11-08) + +## v0.8.3 (2017-11-08) + +## v0.8.2 (2017-10-08) + +## v0.8.1 (2017-10-08) + +## v0.8.0 (2017-10-08) + +### Feat + +- **cz**: jira smart commits + +## v0.7.0 (2017-10-08) + +### Refactor + +- **cli**: renamed all to ls command +- **cz**: renamed angular cz to conventional changelog cz + +## v0.6.0 (2017-10-08) + +### Feat + +- info command for angular -## v1.1.0 +## v0.5.0 (2017-10-07) -### Features +## v0.4.0 (2017-10-07) -- auto bump version based on conventional commits using sem ver -- pyproject support (see [pyproject.toml](./pyproject.toml) for usage) +## v0.3.0 (2017-10-07) -## v1.0.0 +## v0.2.0 (2017-10-07) -### Features +### Feat -- more documentation -- added tests -- support for conventional commits 1.0.0 +- **config**: new loads from ~/.cz and working project .cz .cz.cfg and setup.cfg +- package discovery -### BREAKING CHANGES +### Refactor -- use of questionary to generate the prompt (so we depend on promptkit 2.0) -- python 3 only +- **cz_angular**: improved messages diff --git a/commitizen/changelog.py b/commitizen/changelog.py index b5f29f2f98..25d090b7dd 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -1,33 +1,35 @@ """ # DESIGN -## Parse CHANGELOG.md +## Metadata CHANGELOG.md -1. Get LATEST VERSION from CONFIG -1. Parse the file version to version -2. Build a dict (tree) of that particular version -3. Transform tree into markdown again +1. Identify irrelevant information (possible: changelog title, first paragraph) +2. Identify Unreleased area +3. Identify latest version (to be able to write on top of it) ## Parse git log 1. get commits between versions 2. filter commits with the current cz rules 3. parse commit information -4. generate tree +4. yield tree nodes +5. format tree nodes +6. produce full tree +7. generate changelog -Options: +Extra: - Generate full or partial changelog +- Include in tree from file all the extra comments added manually """ import re -from typing import Dict, Generator, Iterable, List +from collections import defaultdict +from typing import Dict, Iterable, List, Optional -MD_VERSION_RE = r"^##\s(?P[a-zA-Z0-9.+]+)\s?\(?(?P[0-9-]+)?\)?" -MD_CHANGE_TYPE_RE = r"^###\s(?P[a-zA-Z0-9.+\s]+)" -MD_MESSAGE_RE = r"^-\s(\*{2}(?P[a-zA-Z0-9]+)\*{2}:\s)?(?P.+)" -md_version_c = re.compile(MD_VERSION_RE) -md_change_type_c = re.compile(MD_CHANGE_TYPE_RE) -md_message_c = re.compile(MD_MESSAGE_RE) +import pkg_resources +from jinja2 import Template +from commitizen import defaults +from commitizen.git import GitCommit, GitProtocol, GitTag CATEGORIES = [ ("fix", "fix"), @@ -42,62 +44,9 @@ ] -def find_version_blocks(filepath: str) -> Generator: - """ - version block: contains all the information about a version. - - E.g: - ``` - ## 1.2.1 (2019-07-20) - - ## Bug fixes - - - username validation not working - - ## Features - - - new login system - - ``` - """ - with open(filepath, "r") as f: - block: list = [] - for line in f: - line = line.strip("\n") - if not line: - continue - - if line.startswith("## "): - if len(block) > 0: - yield block - block = [line] - else: - block.append(line) - yield block - - -def parse_md_version(md_version: str) -> Dict: - m = md_version_c.match(md_version) - if not m: - return {} - return m.groupdict() - - -def parse_md_change_type(md_change_type: str) -> Dict: - m = md_change_type_c.match(md_change_type) - if not m: - return {} - return m.groupdict() - - -def parse_md_message(md_message: str) -> Dict: - m = md_message_c.match(md_message) - if not m: - return {} - return m.groupdict() - - def transform_change_type(change_type: str) -> str: + # TODO: Use again to parse, for this we have to wait until the maps get + # defined again. _change_type_lower = change_type.lower() for match_value, output in CATEGORIES: if re.search(match_value, _change_type_lower): @@ -106,28 +55,80 @@ def transform_change_type(change_type: str) -> str: raise ValueError(f"Could not match a change_type with {change_type}") -def generate_block_tree(block: List[str]) -> Dict: - tree: Dict = {"commits": []} - change_type = None - for line in block: - if line.startswith("## "): - change_type = None - tree = {**tree, **parse_md_version(line)} - elif line.startswith("### "): - result = parse_md_change_type(line) - if not result: - continue - change_type = transform_change_type(result.get("change_type", "")) - - elif line.startswith("- "): - commit = parse_md_message(line) - commit["change_type"] = change_type - tree["commits"].append(commit) - else: - print("it's something else: ", line) - return tree - - -def generate_full_tree(blocks: Iterable) -> Iterable[Dict]: - for block in blocks: - yield generate_block_tree(block) +def get_commit_tag(commit: GitProtocol, tags: List[GitProtocol]) -> Optional[GitTag]: + """""" + try: + tag_index = tags.index(commit) + except ValueError: + return None + else: + tag = tags[tag_index] + return tag + + +def generate_tree_from_commits( + commits: List[GitCommit], + tags: List[GitTag], + commit_parser: str, + changelog_pattern: str = defaults.bump_pattern, +) -> Iterable[Dict]: + pat = re.compile(changelog_pattern) + map_pat = re.compile(commit_parser) + # Check if the latest commit is not tagged + latest_commit = commits[0] + current_tag: Optional[GitTag] = get_commit_tag(latest_commit, tags) + + current_tag_name: str = "Unreleased" + current_tag_date: str = "" + if current_tag is not None and current_tag.name: + current_tag_name = current_tag.name + current_tag_date = current_tag.date + + changes: Dict = defaultdict(list) + used_tags: List = [current_tag] + for commit in commits: + commit_tag = get_commit_tag(commit, tags) + + if commit_tag is not None and commit_tag not in used_tags: + used_tags.append(commit_tag) + yield { + "version": current_tag_name, + "date": current_tag_date, + "changes": changes, + } + # TODO: Check if tag matches the version pattern, otherwie skip it. + # This in order to prevent tags that are not versions. + current_tag_name = commit_tag.name + current_tag_date = commit_tag.date + changes = defaultdict(list) + + matches = pat.match(commit.message) + if not matches: + continue + + message = map_pat.match(commit.message) + message_body = map_pat.match(commit.body) + if message: + parsed_message: Dict = message.groupdict() + # change_type becomes optional by providing None + change_type = parsed_message.pop("change_type", None) + changes[change_type].append(parsed_message) + if message_body: + parsed_message_body: Dict = message_body.groupdict() + change_type = parsed_message_body.pop("change_type", None) + changes[change_type].append(parsed_message_body) + + yield { + "version": current_tag_name, + "date": current_tag_date, + "changes": changes, + } + + +def render_changelog(tree: Iterable) -> str: + template_file = pkg_resources.resource_string( + __name__, "templates/keep_a_changelog_template.j2" + ).decode("utf-8") + jinja_template = Template(template_file, trim_blocks=True) + changelog: str = jinja_template.render(tree=tree) + return changelog diff --git a/commitizen/changelog_parser.py b/commitizen/changelog_parser.py new file mode 100644 index 0000000000..c945cbdb4f --- /dev/null +++ b/commitizen/changelog_parser.py @@ -0,0 +1,133 @@ +""" +# DESIGN + +## Parse CHANGELOG.md + +1. Get LATEST VERSION from CONFIG +1. Parse the file version to version +2. Build a dict (tree) of that particular version +3. Transform tree into markdown again +""" +import re +from collections import defaultdict +from typing import Dict, Generator, Iterable, List + +MD_VERSION_RE = r"^##\s(?P[a-zA-Z0-9.+]+)\s?\(?(?P[0-9-]+)?\)?" +MD_CHANGE_TYPE_RE = r"^###\s(?P[a-zA-Z0-9.+\s]+)" +MD_MESSAGE_RE = ( + r"^-\s(\*{2}(?P[a-zA-Z0-9]+)\*{2}:\s)?(?P.+)(?P!)?" +) +md_version_c = re.compile(MD_VERSION_RE) +md_change_type_c = re.compile(MD_CHANGE_TYPE_RE) +md_message_c = re.compile(MD_MESSAGE_RE) + + +CATEGORIES = [ + ("fix", "fix"), + ("breaking", "BREAKING CHANGES"), + ("feat", "feat"), + ("refactor", "refactor"), + ("perf", "perf"), + ("test", "test"), + ("build", "build"), + ("ci", "ci"), + ("chore", "chore"), +] + + +def find_version_blocks(filepath: str) -> Generator: + """ + version block: contains all the information about a version. + + E.g: + ``` + ## 1.2.1 (2019-07-20) + + ### Fix + + - username validation not working + + ### Feat + + - new login system + + ``` + """ + with open(filepath, "r") as f: + block: list = [] + for line in f: + line = line.strip("\n") + if not line: + continue + + if line.startswith("## "): + if len(block) > 0: + yield block + block = [line] + else: + block.append(line) + yield block + + +def parse_md_version(md_version: str) -> Dict: + m = md_version_c.match(md_version) + if not m: + return {} + return m.groupdict() + + +def parse_md_change_type(md_change_type: str) -> Dict: + m = md_change_type_c.match(md_change_type) + if not m: + return {} + return m.groupdict() + + +def parse_md_message(md_message: str) -> Dict: + m = md_message_c.match(md_message) + if not m: + return {} + return m.groupdict() + + +def transform_change_type(change_type: str) -> str: + # TODO: Use again to parse, for this we have to wait until the maps get + # defined again. + _change_type_lower = change_type.lower() + for match_value, output in CATEGORIES: + if re.search(match_value, _change_type_lower): + return output + else: + raise ValueError(f"Could not match a change_type with {change_type}") + + +def generate_block_tree(block: List[str]) -> Dict: + # tree: Dict = {"commits": []} + changes: Dict = defaultdict(list) + tree: Dict = {"changes": changes} + + change_type = None + for line in block: + if line.startswith("## "): + # version identified + change_type = None + tree = {**tree, **parse_md_version(line)} + elif line.startswith("### "): + # change_type identified + result = parse_md_change_type(line) + if not result: + continue + change_type = result.get("change_type", "").lower() + + elif line.startswith("- "): + # message identified + commit = parse_md_message(line) + changes[change_type].append(commit) + else: + print("it's something else: ", line) + return tree + + +def generate_full_tree(blocks: Iterable) -> Iterable[Dict]: + for block in blocks: + yield generate_block_tree(block) diff --git a/commitizen/cli.py b/commitizen/cli.py index 9d5130dce9..94eb2ca845 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -131,6 +131,15 @@ "default": False, "help": "show changelog to stdout", }, + { + "name": "--incremental", + "action": "store_true", + "default": False, + "help": ( + "generates changelog from last created version, " + "useful if the changelog has been manually modified" + ), + }, { "name": "--file-name", "help": "file name of changelog (default: 'CHANGELOG.md')", diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index b9bd966d27..016c320a31 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -4,9 +4,9 @@ import pkg_resources from jinja2 import Template -from commitizen import factory, git, out +from commitizen import changelog, factory, git, out from commitizen.config import BaseConfig -from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP +from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP, TAG_FAILED class Changelog: @@ -17,63 +17,73 @@ def __init__(self, config: BaseConfig, args): self.cz = factory.commiter_factory(self.config) self.file_name = args["file_name"] or self.config.settings.get("changelog_file") + self.incremental = args["incremental"] self.dry_run = args["dry_run"] self.start_rev = args["start_rev"] def __call__(self): - changelog_map = self.cz.changelog_map + # changelog_map = self.cz.changelog_map + commit_parser = self.cz.commit_parser changelog_pattern = self.cz.changelog_pattern - if not changelog_map or not changelog_pattern: + + if not changelog_pattern or not commit_parser: out.error( f"'{self.config.settings['name']}' rule does not support changelog" ) raise SystemExit(NO_PATTERN_MAP) - - pat = re.compile(changelog_pattern) + # pat = re.compile(changelog_pattern) commits = git.get_commits(start=self.start_rev) if not commits: out.error("No commits found") raise SystemExit(NO_COMMITS_FOUND) - tag_map = {tag.rev: tag.name for tag in git.get_tags()} - - entries = OrderedDict() - # The latest commit is not tagged - latest_commit = commits[0] - if latest_commit.rev not in tag_map: - current_key = "Unreleased" - entries[current_key] = OrderedDict( - {value: [] for value in changelog_map.values()} - ) - else: - current_key = tag_map[latest_commit.rev] - - for commit in commits: - if commit.rev in tag_map: - current_key = tag_map[commit.rev] - entries[current_key] = OrderedDict( - {value: [] for value in changelog_map.values()} - ) - - matches = pat.match(commit.message) - if not matches: - continue - - processed_commit = self.cz.process_commit(commit.message) - for group_name, commit_type in changelog_map.items(): - if matches.group(group_name): - entries[current_key][commit_type].append(processed_commit) - break - - template_file = pkg_resources.resource_string( - __name__, "../templates/keep_a_changelog_template.j2" - ).decode("utf-8") - jinja_template = Template(template_file) - changelog_str = jinja_template.render(entries=entries) + tags = git.get_tags() + if not tags: + tags = [] + + tree = changelog.generate_tree_from_commits( + commits, tags, commit_parser, changelog_pattern + ) + changelog_out = changelog.render_changelog(tree) + # tag_map = {tag.rev: tag.name for tag in git.get_tags()} + + # entries = OrderedDict() + # # The latest commit is not tagged + # latest_commit = commits[0] + # if latest_commit.rev not in tag_map: + # current_key = "Unreleased" + # entries[current_key] = OrderedDict( + # {value: [] for value in changelog_map.values()} + # ) + # else: + # current_key = tag_map[latest_commit.rev] + + # for commit in commits: + # if commit.rev in tag_map: + # current_key = tag_map[commit.rev] + # entries[current_key] = OrderedDict( + # {value: [] for value in changelog_map.values()} + # ) + + # matches = pat.match(commit.message) + # if not matches: + # continue + + # processed_commit = self.cz.process_commit(commit.message) + # for group_name, commit_type in changelog_map.items(): + # if matches.group(group_name): + # entries[current_key][commit_type].append(processed_commit) + # break + + # template_file = pkg_resources.resource_string( + # __name__, "../templates/keep_a_changelog_template.j2" + # ).decode("utf-8") + # jinja_template = Template(template_file) + # changelog_str = jinja_template.render(entries=entries) if self.dry_run: - out.write(changelog_str) + out.write(changelog_out) raise SystemExit(0) with open(self.file_name, "w") as changelog_file: - changelog_file.write(changelog_str) + changelog_file.write(changelog_out) diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index ff6147d29f..207be89d1d 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -23,6 +23,7 @@ class BaseCommitizen(metaclass=ABCMeta): ("text", ""), ("disabled", "fg:#858585 italic"), ] + commit_parser: Optional[str] = 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 8dc4cec623..b932bd6682 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -1,6 +1,5 @@ import os import re -from collections import OrderedDict from typing import Any, Dict, List from commitizen import defaults @@ -31,10 +30,8 @@ def parse_subject(text): class ConventionalCommitsCz(BaseCommitizen): bump_pattern = defaults.bump_pattern bump_map = defaults.bump_map - changelog_pattern = ( - r"(?P.*\n\nBREAKING CHANGE)|(?P^feat)|(?P^fix)" - ) - changelog_map = OrderedDict({"break": "breaking", "feat": "feat", "fix": "fix"}) + commit_parser = defaults.commit_parser + changelog_pattern = defaults.bump_pattern def questions(self) -> List[Dict[str, Any]]: questions: List[Dict[str, Any]] = [ @@ -194,5 +191,5 @@ def process_commit(self, commit: str) -> str: pat = re.compile(self.schema_pattern()) m = re.match(pat, commit) if m is None: - return '' + return "" return m.group(3).strip() diff --git a/commitizen/defaults.py b/commitizen/defaults.py index a694a11840..070b415646 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -32,3 +32,5 @@ ) ) bump_message = "bump: version $current_version → $new_version" + +commit_parser = r"^(?Pfeat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?:\s(?P.*)?" # noqa diff --git a/commitizen/git.py b/commitizen/git.py index 058ba89a00..feac0697b0 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -3,12 +3,21 @@ from tempfile import NamedTemporaryFile from typing import List, Optional +from typing_extensions import Protocol + from commitizen import cmd +class GitProtocol(Protocol): + rev: str + name: str + + class GitObject: - def __eq__(self, other): - if not isinstance(other, GitObject): + rev: str + + def __eq__(self, other) -> bool: + if not hasattr(other, "rev"): return False return self.rev == other.rev @@ -34,7 +43,7 @@ def __init__(self, name, rev, date): self.date = date.strip() def __repr__(self): - return f"{self.name} ({self.rev})" + return f"GitTag('{self.name}', '{self.rev}', '{self.date}')" def tag(tag: str): diff --git a/commitizen/templates/keep_a_changelog_template.j2 b/commitizen/templates/keep_a_changelog_template.j2 index d3fba14668..a0fadb0223 100644 --- a/commitizen/templates/keep_a_changelog_template.j2 +++ b/commitizen/templates/keep_a_changelog_template.j2 @@ -1,13 +1,19 @@ -# CHANGELOG - -{% for entry_key, entry_value in entries.items() -%} -## {{entry_key}} -{% for type, commits in entry_value.items() -%} -{%- if commits -%} -### {{type}} -{% for commit in commits-%} -- {{commit}} -{% endfor %} +{% for entry in tree %} + +## {{ entry.version }} {% if entry.date %}({{ entry.date }}){% endif %} + +{% for change_key, changes in entry.changes.items() %} + +{% if change_key %} +### {{ change_key|title }} {% endif %} -{%- endfor %} -{%- endfor %} + +{% for change in changes %} +{% if change.scope %} +- **{{ change.scope }}**: {{ change.message }} +{% else %} +- {{ change.message }} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000000..db8d0bc6ed --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,49 @@ +## About + +This command will generate a changelog following the commiting rules established. + +## Usage + +In the command line run + +```bash +cz changelog +``` + +```bash +cz ch +``` + +## Description + +These are the variables used by the changelog generator. + +```md +# () + +## + +- ****: +``` + +It will create a full of the above block per version found in the tags. +And it will create a list of the commits found. +The `change_type` and the `scope` are optional, they don't need to be provided, +but if your regex does they will be rendered. + +The format followed by the changelog is the one from [keep a changelog][keepachangelog] +and the following variables are expected: + +| Variable | Description | Source | +| ------------- | ---------------------------------------------------------------------------------------------- | -------------- | +| `version` | Version number which should follow [semver][semver] | `tags` | +| `date` | Date in which the tag was created | `tags` | +| `change_type` | The group where the commit belongs to, this is optional. Example: fix | `commit regex` | +| `message`\* | Information extracted from the commit message | `commit regex` | +| `scope` | Contextual information. Should be parsed using the regex from the message, it will be **bold** | `commit regex` | +| `breaking` | Wether is a breaking change or not | `commit regex` | + +- **required**: is the only one required to be parsed by the regex + +[keepachangelog]: https://keepachangelog.com/ +[semver]: https://semver.org/ diff --git a/pyproject.toml b/pyproject.toml index 97add2bf9c..7a2685a783 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ termcolor = "^1.1" packaging = ">=19,<21" tomlkit = "^0.5.3" jinja2 = "^2.10.3" +typing = "^3.7.4" [tool.poetry.dev-dependencies] ipython = "^7.2" diff --git a/tests/CHANGELOG_FOR_TEST.md b/tests/CHANGELOG_FOR_TEST.md new file mode 100644 index 0000000000..7605bbed47 --- /dev/null +++ b/tests/CHANGELOG_FOR_TEST.md @@ -0,0 +1,129 @@ + +## v1.2.0 (2019-04-19) + +### Feat + +- custom cz plugins now support bumping version + +## v1.1.1 (2019-04-18) + +### Refactor + +- changed stdout statements +- **schema**: command logic removed from commitizen base +- **info**: command logic removed from commitizen base +- **example**: command logic removed from commitizen base +- **commit**: moved most of the commit logic to the commit command + +### 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 + +- new working bump command +- create version tag +- update given files with new version +- **config**: new set key, used to set version to cfg +- support for pyproject.toml +- first semantic version bump implementaiton + +### Fix + +- removed all from commit +- fix config file not working + +### Refactor + +- added commands folder, better integration with decli + +## v1.0.0 (2019-03-01) + +### Refactor + +- removed delegator, added decli and many tests + +### Breaking Change + +- API is stable + +## 1.0.0b2 (2019-01-18) + +## v1.0.0b1 (2019-01-17) + +### Feat + +- py3 only, tests and conventional commits 1.0 + +## v0.9.11 (2018-12-17) + +### Fix + +- **config**: load config reads in order without failing if there is no commitizen section + +## v0.9.10 (2018-09-22) + +### Fix + +- parse scope (this is my punishment for not having tests) + +## v0.9.9 (2018-09-22) + +### Fix + +- parse scope empty + +## v0.9.8 (2018-09-22) + +### Fix + +- **scope**: parse correctly again + +## v0.9.7 (2018-09-22) + +### Fix + +- **scope**: parse correctly + +## v0.9.6 (2018-09-19) + +### Refactor + +- **conventionalCommit**: moved fitlers to questions instead of message + +### Fix + +- **manifest**: inluded missing files + +## v0.9.5 (2018-08-24) + +### Fix + +- **config**: home path for python versions between 3.0 and 3.5 + +## v0.9.4 (2018-08-02) + +### Feat + +- **cli**: added version + +## v0.9.3 (2018-07-28) + +### Feat + +- **commiter**: conventional commit is a bit more intelligent now + +## v0.9.2 (2017-11-11) + +### Refactor + +- renamed conventional_changelog to conventional_commits, not backward compatible + +## v0.9.1 (2017-11-11) + +### Fix + +- **setup.py**: future is now required for every python version diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index a053e6c238..981cfa654c 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -28,7 +28,11 @@ def test_changlog_from_start(mocker, capsys): cli.main() out, _ = capsys.readouterr() - assert out == "# CHANGELOG\n\n## Unreleased\n### feat\n- new file\n\n\n" + + assert ( + out + == "\n## Unreleased \n\n### Refactor\n\n- not in changelog\n\n### Feat\n\n- new file\n\n" + ) @pytest.mark.usefixtures("tmp_commitizen_project") @@ -51,10 +55,7 @@ def test_changlog_from_version_zero_point_two(mocker, capsys): cli.main() out, _ = capsys.readouterr() - assert ( - out - == "# CHANGELOG\n\n## Unreleased\n### feat\n- after 0.2\n- after 0.2.0\n\n\n" - ) + assert out == "\n## Unreleased \n\n### Feat\n\n- after 0.2\n- after 0.2.0\n\n" @pytest.mark.usefixtures("tmp_commitizen_project") diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 8052832f25..94c3c47017 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,214 +1,703 @@ -import os - import pytest -from commitizen import changelog - -COMMIT_LOG = [ - "bump: version 1.5.0 → 1.5.1", - "", - "Merge pull request #29 from esciara/issue_28", - "fix: #28 allows poetry add on py36 envs", - "fix: #28 allows poetry add on py36 envs", - "", - "Merge pull request #26 from Woile/dependabot/pip/black-tw-19.3b0", - "chore(deps-dev): update black requirement from ^18.3-alpha.0 to ^19.3b0", - "Merge pull request #27 from Woile/dependabot/pip/mypy-tw-0.701", - "chore(deps-dev): update mypy requirement from ^0.700.0 to ^0.701", - "chore(deps-dev): update mypy requirement from ^0.700.0 to ^0.701", - ( - "Updates the requirements on " - "[mypy](https://github.com/python/mypy) to permit the latest version." - ), - "- [Release notes](https://github.com/python/mypy/releases)", - "- [Commits](https://github.com/python/mypy/compare/v0.700...v0.701)", - "", - "Signed-off-by: dependabot[bot] ", - "chore(deps-dev): update black requirement from ^18.3-alpha.0 to ^19.3b0", - ( - "Updates the requirements on [black](https://github.com/ambv/black)" - " to permit the latest version." - ), - "- [Release notes](https://github.com/ambv/black/releases)", - "- [Commits](https://github.com/ambv/black/commits)", - "", - "Signed-off-by: dependabot[bot] ", - "bump: version 1.4.0 → 1.5.0", - "", - "docs: add info about extra pattern in the files when bumping", - "", - ( - "feat(bump): it is now possible to specify a pattern " - "in the files attr to replace the version" - ), - "", +from commitizen import changelog, defaults, git + +COMMITS_DATA = [ + { + "rev": "141ee441c9c9da0809c554103a558eb17c30ed17", + "title": "bump: version 1.1.1 → 1.2.0", + "body": "", + }, + { + "rev": "6c4948501031b7d6405b54b21d3d635827f9421b", + "title": "docs: how to create custom bumps", + "body": "", + }, + { + "rev": "ddd220ad515502200fe2dde443614c1075d26238", + "title": "feat: custom cz plugins now support bumping version", + "body": "", + }, + { + "rev": "ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc", + "title": "docs: added bump gif", + "body": "", + }, + { + "rev": "56c8a8da84e42b526bcbe130bd194306f7c7e813", + "title": "bump: version 1.1.0 → 1.1.1", + "body": "", + }, + { + "rev": "74c6134b1b2e6bb8b07ed53410faabe99b204f36", + "title": "refactor: changed stdout statements", + "body": "", + }, + { + "rev": "cbc7b5f22c4e74deff4bc92d14e19bd93524711e", + "title": "fix(bump): commit message now fits better with semver", + "body": "", + }, + { + "rev": "1ba46f2a63cb9d6e7472eaece21528c8cd28b118", + "title": "fix: conventional commit 'breaking change' in body instead of title", + "body": "closes #16", + }, + { + "rev": "c35dbffd1bb98bb0b3d1593797e79d1c3366af8f", + "title": "refactor(schema): command logic removed from commitizen base", + "body": "", + }, + { + "rev": "25313397a4ac3dc5b5c986017bee2a614399509d", + "title": "refactor(info): command logic removed from commitizen base", + "body": "", + }, + { + "rev": "d2f13ac41b4e48995b3b619d931c82451886e6ff", + "title": "refactor(example): command logic removed from commitizen base", + "body": "", + }, + { + "rev": "d839e317e5b26671b010584ad8cc6bf362400fa1", + "title": "refactor(commit): moved most of the commit logic to the commit command", + "body": "", + }, + { + "rev": "12d0e65beda969f7983c444ceedc2a01584f4e08", + "title": "docs(README): updated documentation url)", + "body": "", + }, + { + "rev": "fb4c85abe51c228e50773e424cbd885a8b6c610d", + "title": "docs: mkdocs documentation", + "body": "", + }, + { + "rev": "17efb44d2cd16f6621413691a543e467c7d2dda6", + "title": "Bump version 1.0.0 → 1.1.0", + "body": "", + }, + { + "rev": "6012d9eecfce8163d75c8fff179788e9ad5347da", + "title": "test: fixed issues with conf", + "body": "", + }, + { + "rev": "0c7fb0ca0168864dfc55d83c210da57771a18319", + "title": "docs(README): some new information about bump", + "body": "", + }, + { + "rev": "cb1dd2019d522644da5bdc2594dd6dee17122d7f", + "title": "feat: new working bump command", + "body": "", + }, + { + "rev": "9c7450f85df6bf6be508e79abf00855a30c3c73c", + "title": "feat: create version tag", + "body": "", + }, + { + "rev": "9f3af3772baab167e3fd8775d37f041440184251", + "title": "docs: added new changelog", + "body": "", + }, + { + "rev": "b0d6a3defbfde14e676e7eb34946409297d0221b", + "title": "feat: update given files with new version", + "body": "", + }, + { + "rev": "d630d07d912e420f0880551f3ac94e933f9d3beb", + "title": "fix: removed all from commit", + "body": "", + }, + { + "rev": "1792b8980c58787906dbe6836f93f31971b1ec2d", + "title": "feat(config): new set key, used to set version to cfg", + "body": "", + }, + { + "rev": "52def1ea3555185ba4b936b463311949907e31ec", + "title": "feat: support for pyproject.toml", + "body": "", + }, + { + "rev": "3127e05077288a5e2b62893345590bf1096141b7", + "title": "feat: first semantic version bump implementaiton", + "body": "", + }, + { + "rev": "fd480ed90a80a6ffa540549408403d5b60d0e90c", + "title": "fix: fix config file not working", + "body": "", + }, + { + "rev": "e4840a059731c0bf488381ffc77e989e85dd81ad", + "title": "refactor: added commands folder, better integration with decli", + "body": "", + }, + { + "rev": "aa44a92d68014d0da98965c0c2cb8c07957d4362", + "title": "Bump version: 1.0.0b2 → 1.0.0", + "body": "", + }, + { + "rev": "58bb709765380dbd46b74ce6e8978515764eb955", + "title": "docs(README): new badges", + "body": "", + }, + { + "rev": "97afb0bb48e72b6feca793091a8a23c706693257", + "title": "Merge pull request #10 from Woile/feat/decli", + "body": "Feat/decli", + }, + { + "rev": "9cecb9224aa7fa68d4afeac37eba2a25770ef251", + "title": "style: black to files", + "body": "", + }, + { + "rev": "f5781d1a2954d71c14ade2a6a1a95b91310b2577", + "title": "ci: added travis", + "body": "", + }, + { + "rev": "80105fb3c6d45369bc0cbf787bd329fba603864c", + "title": "refactor: removed delegator, added decli and many tests", + "body": "BREAKING CHANGE: API is stable", + }, + { + "rev": "a96008496ffefb6b1dd9b251cb479eac6a0487f7", + "title": "docs: updated test command", + "body": "", + }, + { + "rev": "aab33d13110f26604fb786878856ec0b9e5fc32b", + "title": "Bump version: 1.0.0b1 → 1.0.0b2", + "body": "", + }, + { + "rev": "b73791563d2f218806786090fb49ef70faa51a3a", + "title": "docs(README): updated to reflect current state", + "body": "", + }, + { + "rev": "7aa06a454fb717408b3657faa590731fb4ab3719", + "title": "Merge pull request #9 from Woile/dev", + "body": "feat: py3 only, tests and conventional commits 1.0", + }, + { + "rev": "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", + "title": "Bump version: 0.9.11 → 1.0.0b1", + "body": "", + }, + { + "rev": "ed830019581c83ba633bfd734720e6758eca6061", + "title": "feat: py3 only, tests and conventional commits 1.0", + "body": "more tests\npyproject instead of Pipfile\nquestionary instead of whaaaaat (promptkit 2.0.0 support)", + }, + { + "rev": "c52eca6f74f844ab3ffbde61d98ef96071e132b7", + "title": "Bump version: 0.9.10 → 0.9.11", + "body": "", + }, + { + "rev": "0326652b2657083929507ee66d4d1a0899e861ba", + "title": "fix(config): load config reads in order without failing if there is no commitizen section", + "body": "Closes #8", + }, + { + "rev": "b3f89892222340150e32631ae6b7aab65230036f", + "title": "Bump version: 0.9.9 → 0.9.10", + "body": "", + }, + { + "rev": "5e837bf8ef0735193597372cd2d85e31a8f715b9", + "title": "fix: parse scope (this is my punishment for not having tests)", + "body": "", + }, + { + "rev": "684e0259cc95c7c5e94854608cd3dcebbd53219e", + "title": "Bump version: 0.9.8 → 0.9.9", + "body": "", + }, + { + "rev": "ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15", + "title": "fix: parse scope empty", + "body": "", + }, + { + "rev": "64168f18d4628718c49689ee16430549e96c5d4b", + "title": "Bump version: 0.9.7 → 0.9.8", + "body": "", + }, + { + "rev": "9d4def716ef235a1fa5ae61614366423fbc8256f", + "title": "fix(scope): parse correctly again", + "body": "", + }, + { + "rev": "33b0bf1a0a4dc60aac45ed47476d2e5473add09e", + "title": "Bump version: 0.9.6 → 0.9.7", + "body": "", + }, + { + "rev": "696885e891ec35775daeb5fec3ba2ab92c2629e1", + "title": "fix(scope): parse correctly", + "body": "", + }, + { + "rev": "bef4a86761a3bda309c962bae5d22ce9b57119e4", + "title": "Bump version: 0.9.5 → 0.9.6", + "body": "", + }, + { + "rev": "72472efb80f08ee3fd844660afa012c8cb256e4b", + "title": "refactor(conventionalCommit): moved fitlers to questions instead of message", + "body": "", + }, + { + "rev": "b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b", + "title": "fix(manifest): inluded missing files", + "body": "", + }, + { + "rev": "3e31714dc737029d96898f412e4ecd2be1bcd0ce", + "title": "Bump version: 0.9.4 → 0.9.5", + "body": "", + }, + { + "rev": "9df721e06595fdd216884c36a28770438b4f4a39", + "title": "fix(config): home path for python versions between 3.0 and 3.5", + "body": "", + }, + { + "rev": "0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f", + "title": "Bump version: 0.9.3 → 0.9.4", + "body": "", + }, + { + "rev": "973c6b3e100f6f69a3fe48bd8ee55c135b96c318", + "title": "feat(cli): added version", + "body": "", + }, + { + "rev": "dacc86159b260ee98eb5f57941c99ba731a01399", + "title": "Bump version: 0.9.2 → 0.9.3", + "body": "", + }, + { + "rev": "4368f3c3cbfd4a1ced339212230d854bc5bab496", + "title": "feat(commiter): conventional commit is a bit more intelligent now", + "body": "", + }, + { + "rev": "da94133288727d35dae9b91866a25045038f2d38", + "title": "docs(README): motivation", + "body": "", + }, + { + "rev": "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", + "title": "Bump version: 0.9.1 → 0.9.2", + "body": "", + }, + { + "rev": "ddc855a637b7879108308b8dbd85a0fd27c7e0e7", + "title": "refactor: renamed conventional_changelog to conventional_commits, not backward compatible", + "body": "", + }, + { + "rev": "46e9032e18a819e466618c7a014bcb0e9981af9e", + "title": "Bump version: 0.9.0 → 0.9.1", + "body": "", + }, + { + "rev": "0fef73cd7dc77a25b82e197e7c1d3144a58c1350", + "title": "fix(setup.py): future is now required for every python version", + "body": "", + }, ] -CHANGELOG_TEMPLATE = """ -## 1.0.0 (2019-07-12) - -### Bug fixes - -- issue in poetry add preventing the installation in py36 -- **users**: lorem ipsum apap - - -### Features - -- it is possible to specify a pattern to be matched in configuration files bump. - -## 0.9 (2019-07-11) - -### Bug fixes - -- holis - -""" - - -@pytest.fixture -def existing_changelog_file(request): - changelog_path = "tests/CHANGELOG.md" - - with open(changelog_path, "w") as f: - f.write(CHANGELOG_TEMPLATE) - - yield changelog_path - - os.remove(changelog_path) - - -def test_read_changelog_blocks(existing_changelog_file): - blocks = changelog.find_version_blocks(existing_changelog_file) - blocks = list(blocks) - amount_of_blocks = len(blocks) - assert amount_of_blocks == 2 - -VERSION_CASES: list = [ - ("## 1.0.0 (2019-07-12)", {"version": "1.0.0", "date": "2019-07-12"}), - ("## 2.3.0a0", {"version": "2.3.0a0", "date": None}), - ("## 0.10.0a0", {"version": "0.10.0a0", "date": None}), - ("## 1.0.0rc0", {"version": "1.0.0rc0", "date": None}), - ("## 1beta", {"version": "1beta", "date": None}), - ( - "## 1.0.0rc1+e20d7b57f3eb (2019-3-24)", - {"version": "1.0.0rc1+e20d7b57f3eb", "date": "2019-3-24"}, - ), - ("### Bug fixes", {}), - ("- issue in poetry add preventing the installation in py36", {}), +TAGS = [ + ("v1.2.0", "141ee441c9c9da0809c554103a558eb17c30ed17", "2019-04-19"), + ("v1.1.1", "56c8a8da84e42b526bcbe130bd194306f7c7e813", "2019-04-18"), + ("v1.1.0", "17efb44d2cd16f6621413691a543e467c7d2dda6", "2019-04-14"), + ("v1.0.0", "aa44a92d68014d0da98965c0c2cb8c07957d4362", "2019-03-01"), + ("1.0.0b2", "aab33d13110f26604fb786878856ec0b9e5fc32b", "2019-01-18"), + ("v1.0.0b1", "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", "2019-01-17"), + ("v0.9.11", "c52eca6f74f844ab3ffbde61d98ef96071e132b7", "2018-12-17"), + ("v0.9.10", "b3f89892222340150e32631ae6b7aab65230036f", "2018-09-22"), + ("v0.9.9", "684e0259cc95c7c5e94854608cd3dcebbd53219e", "2018-09-22"), + ("v0.9.8", "64168f18d4628718c49689ee16430549e96c5d4b", "2018-09-22"), + ("v0.9.7", "33b0bf1a0a4dc60aac45ed47476d2e5473add09e", "2018-09-22"), + ("v0.9.6", "bef4a86761a3bda309c962bae5d22ce9b57119e4", "2018-09-19"), + ("v0.9.5", "3e31714dc737029d96898f412e4ecd2be1bcd0ce", "2018-08-24"), + ("v0.9.4", "0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f", "2018-08-02"), + ("v0.9.3", "dacc86159b260ee98eb5f57941c99ba731a01399", "2018-07-28"), + ("v0.9.2", "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", "2017-11-11"), + ("v0.9.1", "46e9032e18a819e466618c7a014bcb0e9981af9e", "2017-11-11"), ] -CATEGORIES_CASES: list = [ - ("## 1.0.0 (2019-07-12)", {}), - ("## 2.3.0a0", {}), - ("### Bug fixes", {"change_type": "Bug fixes"}), - ("### Features", {"change_type": "Features"}), - ("- issue in poetry add preventing the installation in py36", {}), -] -CATEGORIES_TRANSFORMATIONS: list = [ - ("Bug fixes", "fix"), - ("Features", "feat"), - ("BREAKING CHANGES", "BREAKING CHANGES"), -] - -MESSAGES_CASES: list = [ - ("## 1.0.0 (2019-07-12)", {}), - ("## 2.3.0a0", {}), - ("### Bug fixes", {}), - ( - "- name no longer accept invalid chars", - {"message": "name no longer accept invalid chars", "scope": None}, - ), - ( - "- **users**: lorem ipsum apap", - {"message": "lorem ipsum apap", "scope": "users"}, - ), -] +@pytest.fixture # type: ignore +def gitcommits() -> list: + commits = [ + git.GitCommit(commit["rev"], commit["title"], commit["body"]) + for commit in COMMITS_DATA + ] + return commits -@pytest.mark.parametrize("test_input,expected", VERSION_CASES) -def test_parse_md_version(test_input, expected): - assert changelog.parse_md_version(test_input) == expected +@pytest.fixture # type: ignore +def tags() -> list: + tags = [git.GitTag(*tag) for tag in TAGS] + return tags -@pytest.mark.parametrize("test_input,expected", CATEGORIES_CASES) -def test_parse_md_change_type(test_input, expected): - assert changelog.parse_md_change_type(test_input) == expected +@pytest.fixture # type: ignore +def changelog_content() -> str: + changelog_path = "tests/CHANGELOG_FOR_TEST.md" + with open(changelog_path, "r") as f: + return f.read() -@pytest.mark.parametrize("test_input,expected", CATEGORIES_TRANSFORMATIONS) -def test_transform_change_type(test_input, expected): - assert changelog.transform_change_type(test_input) == expected +def test_get_commit_tag_is_a_version(gitcommits, tags): + commit = gitcommits[0] + tag = git.GitTag(*TAGS[0]) + current_key = changelog.get_commit_tag(commit, tags) + assert current_key == tag -@pytest.mark.parametrize("test_input,expected", MESSAGES_CASES) -def test_parse_md_message(test_input, expected): - assert changelog.parse_md_message(test_input) == expected +def test_get_commit_tag_is_None(gitcommits, tags): + commit = gitcommits[1] + current_key = changelog.get_commit_tag(commit, tags) + assert current_key is None -def test_transform_change_type_fail(): - with pytest.raises(ValueError) as excinfo: - changelog.transform_change_type("Bugs") - assert "Could not match a change_type" in str(excinfo.value) +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 + ) -def test_generate_block_tree(existing_changelog_file): - blocks = changelog.find_version_blocks(existing_changelog_file) - block = next(blocks) - tree = changelog.generate_block_tree(block) - assert tree == { - "commits": [ - { - "scope": None, - "message": "issue in poetry add preventing the installation in py36", - "change_type": "fix", + 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", + } + ] }, - {"scope": "users", "message": "lorem ipsum apap", "change_type": "fix"}, - { - "scope": None, - "message": ( - "it is possible to specify a pattern to be matched " - "in configuration files bump." - ), - "change_type": "feat", + }, + { + "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": "1.0.0", - "date": "2019-07-12", - } - - -def test_generate_full_tree(existing_changelog_file): - blocks = changelog.find_version_blocks(existing_changelog_file) - tree = list(changelog.generate_full_tree(blocks)) - - assert tree == [ + }, + { + "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 implementaiton", + }, + ], + "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": "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.10", + "date": "2018-09-22", + "changes": { + "fix": [ + { + "scope": None, + "breaking": None, + "message": "parse scope (this is my punishment for not having tests)", + } + ] + }, + }, { - "commits": [ - { - "scope": None, - "message": ( - "issue in poetry add preventing the installation in py36" - ), - "change_type": "fix", - }, - {"scope": "users", "message": "lorem ipsum apap", "change_type": "fix"}, - { - "scope": None, - "message": ( - "it is possible to specify a pattern to be matched " - "in configuration files bump." - ), - "change_type": "feat", - }, - ], - "version": "1.0.0", - "date": "2019-07-12", + "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", + } + ] + }, }, { - "commits": [{"scope": None, "message": "holis", "change_type": "fix"}], - "version": "0.9", - "date": "2019-07-11", + "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 fitlers to questions instead of message", + } + ], + "fix": [ + { + "scope": "manifest", + "breaking": None, + "message": "inluded 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.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": "commiter", + "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.1", + "date": "2017-11-11", + "changes": { + "fix": [ + { + "scope": "setup.py", + "breaking": None, + "message": "future is now required for every python version", + } + ] + }, + }, + ) + + +def test_render_changelog(gitcommits, tags, changelog_content): + parser = defaults.commit_parser + changelog_pattern = defaults.bump_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + result = changelog.render_changelog(tree) + assert result == changelog_content + + +def test_render_changelog_unreleased(gitcommits): + some_commits = gitcommits[:7] + parser = defaults.commit_parser + changelog_pattern = defaults.bump_pattern + tree = changelog.generate_tree_from_commits( + some_commits, [], parser, changelog_pattern + ) + result = changelog.render_changelog(tree) + assert "Unreleased" in result + + +def test_render_changelog_tag_and_unreleased(gitcommits, tags): + some_commits = gitcommits[:7] + single_tag = [ + tag for tag in tags if tag.rev == "56c8a8da84e42b526bcbe130bd194306f7c7e813" ] + + parser = defaults.commit_parser + changelog_pattern = defaults.bump_pattern + tree = changelog.generate_tree_from_commits( + some_commits, single_tag, parser, changelog_pattern + ) + result = changelog.render_changelog(tree) + + assert "Unreleased" in result + assert "## v1.1.1" in result diff --git a/tests/test_changelog_parser.py b/tests/test_changelog_parser.py new file mode 100644 index 0000000000..352447ae26 --- /dev/null +++ b/tests/test_changelog_parser.py @@ -0,0 +1,194 @@ +import os + +import pytest + +from commitizen import changelog_parser + +CHANGELOG_TEMPLATE = """ +## 1.0.0 (2019-07-12) + +### Fix + +- issue in poetry add preventing the installation in py36 +- **users**: lorem ipsum apap + +### Feat + +- it is possible to specify a pattern to be matched in configuration files bump. + +## 0.9 (2019-07-11) + +### Fix + +- holis + +""" + + +@pytest.fixture # type: ignore +def changelog_content() -> str: + changelog_path = "tests/CHANGELOG_FOR_TEST.md" + with open(changelog_path, "r") as f: + return f.read() + + +@pytest.fixture +def existing_changelog_file(request): + changelog_path = "tests/CHANGELOG.md" + + with open(changelog_path, "w") as f: + f.write(CHANGELOG_TEMPLATE) + + yield changelog_path + + os.remove(changelog_path) + + +def test_read_changelog_blocks(existing_changelog_file): + blocks = changelog_parser.find_version_blocks(existing_changelog_file) + blocks = list(blocks) + amount_of_blocks = len(blocks) + assert amount_of_blocks == 2 + + +VERSION_CASES: list = [ + ("## 1.0.0 (2019-07-12)", {"version": "1.0.0", "date": "2019-07-12"}), + ("## 2.3.0a0", {"version": "2.3.0a0", "date": None}), + ("## 0.10.0a0", {"version": "0.10.0a0", "date": None}), + ("## 1.0.0rc0", {"version": "1.0.0rc0", "date": None}), + ("## 1beta", {"version": "1beta", "date": None}), + ( + "## 1.0.0rc1+e20d7b57f3eb (2019-3-24)", + {"version": "1.0.0rc1+e20d7b57f3eb", "date": "2019-3-24"}, + ), + ("### Bug fixes", {}), + ("- issue in poetry add preventing the installation in py36", {}), +] + +CATEGORIES_CASES: list = [ + ("## 1.0.0 (2019-07-12)", {}), + ("## 2.3.0a0", {}), + ("### Bug fixes", {"change_type": "Bug fixes"}), + ("### Features", {"change_type": "Features"}), + ("- issue in poetry add preventing the installation in py36", {}), +] + +CATEGORIES_TRANSFORMATIONS: list = [ + ("Bug fixes", "fix"), + ("Features", "feat"), + ("BREAKING CHANGES", "BREAKING CHANGES"), +] + +MESSAGES_CASES: list = [ + ("## 1.0.0 (2019-07-12)", {}), + ("## 2.3.0a0", {}), + ("### Fix", {}), + ( + "- name no longer accept invalid chars", + { + "message": "name no longer accept invalid chars", + "scope": None, + "breaking": None, + }, + ), + ( + "- **users**: lorem ipsum apap", + {"message": "lorem ipsum apap", "scope": "users", "breaking": None}, + ), +] + + +@pytest.mark.parametrize("test_input,expected", VERSION_CASES) +def test_parse_md_version(test_input, expected): + assert changelog_parser.parse_md_version(test_input) == expected + + +@pytest.mark.parametrize("test_input,expected", CATEGORIES_CASES) +def test_parse_md_change_type(test_input, expected): + assert changelog_parser.parse_md_change_type(test_input) == expected + + +@pytest.mark.parametrize("test_input,expected", CATEGORIES_TRANSFORMATIONS) +def test_transform_change_type(test_input, expected): + assert changelog_parser.transform_change_type(test_input) == expected + + +@pytest.mark.parametrize("test_input,expected", MESSAGES_CASES) +def test_parse_md_message(test_input, expected): + assert changelog_parser.parse_md_message(test_input) == expected + + +def test_transform_change_type_fail(): + with pytest.raises(ValueError) as excinfo: + changelog_parser.transform_change_type("Bugs") + assert "Could not match a change_type" in str(excinfo.value) + + +def test_generate_block_tree(existing_changelog_file): + blocks = changelog_parser.find_version_blocks(existing_changelog_file) + block = next(blocks) + tree = changelog_parser.generate_block_tree(block) + assert tree == { + "changes": { + "fix": [ + { + "scope": None, + "breaking": None, + "message": "issue in poetry add preventing the installation in py36", + }, + {"scope": "users", "breaking": None, "message": "lorem ipsum apap"}, + ], + "feat": [ + { + "scope": None, + "breaking": None, + "message": ( + "it is possible to specify a pattern to be matched " + "in configuration files bump." + ), + } + ], + }, + "version": "1.0.0", + "date": "2019-07-12", + } + + +def test_generate_full_tree(existing_changelog_file): + blocks = changelog_parser.find_version_blocks(existing_changelog_file) + tree = list(changelog_parser.generate_full_tree(blocks)) + + assert tree == [ + { + "changes": { + "fix": [ + { + "scope": None, + "message": "issue in poetry add preventing the installation in py36", + "breaking": None, + }, + { + "scope": "users", + "message": "lorem ipsum apap", + "breaking": None, + }, + ], + "feat": [ + { + "scope": None, + "message": "it is possible to specify a pattern to be matched in configuration files bump.", + "breaking": None, + }, + ], + }, + "version": "1.0.0", + "date": "2019-07-12", + }, + { + "changes": { + "fix": [{"scope": None, "message": "holis", "breaking": None}], + }, + "version": "0.9", + "date": "2019-07-11", + }, + ] From 84471b4e8b767e89cbe4a5e29181d47e882e4377 Mon Sep 17 00:00:00 2001 From: santiago fraire Date: Mon, 27 Apr 2020 12:06:09 +0200 Subject: [PATCH 30/35] feat(changelog): add incremental flag --- commitizen/changelog.py | 130 +++++++++++++-- commitizen/cli.py | 21 ++- commitizen/commands/bump.py | 13 ++ commitizen/commands/changelog.py | 121 ++++++++------ commitizen/commands/init.py | 4 +- commitizen/defaults.py | 1 + commitizen/error_codes.py | 3 + commitizen/git.py | 2 + docs/changelog.md | 17 +- pyproject.toml | 1 + tests/commands/test_changelog_command.py | 203 ++++++++++++++++++++--- tests/test_changelog_meta.py | 165 ++++++++++++++++++ tests/test_changelog_parser.py | 2 +- 13 files changed, 591 insertions(+), 92 deletions(-) create mode 100644 tests/test_changelog_meta.py diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 25d090b7dd..9596dcb027 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -18,8 +18,12 @@ 7. generate changelog Extra: -- Generate full or partial changelog -- Include in tree from file all the extra comments added manually +- [x] Generate full or partial changelog +- [x] Include in tree from file all the extra comments added manually +- [ ] Add unreleased value +- [ ] hook after message is parsed (add extra information like hyperlinks) +- [ ] hook after changelog is generated (api calls) +- [ ] add support for change_type maps """ import re from collections import defaultdict @@ -29,7 +33,7 @@ from jinja2 import Template from commitizen import defaults -from commitizen.git import GitCommit, GitProtocol, GitTag +from commitizen.git import GitCommit, GitTag CATEGORIES = [ ("fix", "fix"), @@ -55,15 +59,9 @@ def transform_change_type(change_type: str) -> str: raise ValueError(f"Could not match a change_type with {change_type}") -def get_commit_tag(commit: GitProtocol, tags: List[GitProtocol]) -> Optional[GitTag]: +def get_commit_tag(commit: GitCommit, tags: List[GitTag]) -> Optional[GitTag]: """""" - try: - tag_index = tags.index(commit) - except ValueError: - return None - else: - tag = tags[tag_index] - return tag + return next((tag for tag in tags if tag.rev == commit.rev), None) def generate_tree_from_commits( @@ -71,6 +69,7 @@ def generate_tree_from_commits( tags: List[GitTag], commit_parser: str, changelog_pattern: str = defaults.bump_pattern, + unreleased_version: Optional[str] = None, ) -> Iterable[Dict]: pat = re.compile(changelog_pattern) map_pat = re.compile(commit_parser) @@ -78,7 +77,7 @@ def generate_tree_from_commits( latest_commit = commits[0] current_tag: Optional[GitTag] = get_commit_tag(latest_commit, tags) - current_tag_name: str = "Unreleased" + current_tag_name: str = unreleased_version or "Unreleased" current_tag_date: str = "" if current_tag is not None and current_tag.name: current_tag_name = current_tag.name @@ -109,6 +108,7 @@ 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) @@ -132,3 +132,109 @@ def render_changelog(tree: Iterable) -> str: jinja_template = Template(template_file, trim_blocks=True) changelog: str = jinja_template.render(tree=tree) return changelog + + +def parse_version_from_markdown(value: str) -> Optional[str]: + if not value.startswith("#"): + return None + m = re.search(defaults.version_parser, value) + if not m: + return None + return m.groupdict().get("version") + + +def parse_title_type_of_line(value: str) -> Optional[str]: + md_title_parser = r"^(?P#+)" + m = re.search(md_title_parser, value) + if not m: + return None + return m.groupdict().get("title") + + +def get_metadata(filepath: str) -> Dict: + unreleased_start: Optional[int] = None + unreleased_end: Optional[int] = None + unreleased_title: Optional[str] = None + latest_version: Optional[str] = None + latest_version_position: Optional[int] = None + with open(filepath, "r") as changelog_file: + for index, line in enumerate(changelog_file): + line = line.strip().lower() + + unreleased: Optional[str] = None + if "unreleased" in line: + unreleased = parse_title_type_of_line(line) + # Try to find beginning and end lines of the unreleased block + if unreleased: + unreleased_start = index + unreleased_title = unreleased + continue + elif ( + isinstance(unreleased_title, str) + and parse_title_type_of_line(line) == unreleased_title + ): + unreleased_end = index + + # Try to find the latest release done + version = parse_version_from_markdown(line) + if version: + latest_version = version + latest_version_position = index + break # there's no need for more info + if unreleased_start is not None and unreleased_end is None: + unreleased_end = index + return { + "unreleased_start": unreleased_start, + "unreleased_end": unreleased_end, + "latest_version": latest_version, + "latest_version_position": latest_version_position, + } + + +def incremental_build(new_content: str, lines: List, metadata: Dict) -> List: + """Takes the original lines and updates with new_content. + + The metadata holds information enough to remove the old unreleased and + where to place the new content + + Arguments: + lines -- the lines from the changelog + new_content -- this should be placed somewhere in the lines + metadata -- information about the changelog + + Returns: + List -- updated lines + """ + unreleased_start = metadata.get("unreleased_start") + unreleased_end = metadata.get("unreleased_end") + latest_version_position = metadata.get("latest_version_position") + skip = False + output_lines: List = [] + for index, line in enumerate(lines): + if index == unreleased_start: + skip = True + elif index == unreleased_end: + skip = False + if ( + latest_version_position is None + or isinstance(latest_version_position, int) + and isinstance(unreleased_end, int) + and latest_version_position > unreleased_end + ): + continue + + if skip: + continue + + if ( + isinstance(latest_version_position, int) + and index == latest_version_position + ): + + output_lines.append(new_content) + output_lines.append("\n") + + output_lines.append(line) + if not isinstance(latest_version_position, int): + output_lines.append(new_content) + return output_lines diff --git a/commitizen/cli.py b/commitizen/cli.py index 94eb2ca845..f5b41a2e2f 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -86,6 +86,12 @@ "action": "store_true", "help": "bump version in the files from the config", }, + { + "name": ["--changelog", "-ch"], + "action": "store_true", + "default": False, + "help": "generate the changelog for the newest version", + }, { "name": "--yes", "action": "store_true", @@ -131,6 +137,17 @@ "default": False, "help": "show changelog to stdout", }, + { + "name": "--file-name", + "help": "file name of changelog (default: 'CHANGELOG.md')", + }, + { + "name": "--unreleased-version", + "help": ( + "set the value for the new version (use the tag value), " + "instead of using unreleased" + ), + }, { "name": "--incremental", "action": "store_true", @@ -140,10 +157,6 @@ "useful if the changelog has been manually modified" ), }, - { - "name": "--file-name", - "help": "file name of changelog (default: 'CHANGELOG.md')", - }, { "name": "--start-rev", "default": None, diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 90ec715744..1d0439e4b9 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -4,6 +4,7 @@ from packaging.version import Version from commitizen import bump, factory, git, out +from commitizen.commands.changelog import Changelog from commitizen.config import BaseConfig from commitizen.error_codes import ( COMMIT_FAILED, @@ -29,6 +30,7 @@ def __init__(self, config: BaseConfig, arguments: dict): }, } self.cz = factory.commiter_factory(self.config) + self.changelog = arguments["changelog"] def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool: """Check if reading the whole git tree up to HEAD is needed.""" @@ -131,6 +133,17 @@ def __call__(self): # noqa: C901 if is_files_only: raise SystemExit() + if self.changelog: + changelog = Changelog( + self.config, + { + "unreleased_version": new_tag_version, + "incremental": True, + "dry_run": dry_run, + }, + ) + changelog() + self.config.set_key("version", new_version.public) c = git.commit(message, args="-a") if c.err: diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 016c320a31..12f43f775d 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -1,12 +1,16 @@ -import re -from collections import OrderedDict - -import pkg_resources -from jinja2 import Template +import os.path +from difflib import SequenceMatcher +from operator import itemgetter +from typing import Dict, List from commitizen import changelog, factory, git, out from commitizen.config import BaseConfig -from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP, TAG_FAILED +from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP, NO_REVISION +from commitizen.git import GitTag + + +def similar(a, b): + return SequenceMatcher(None, a, b).ratio() class Changelog: @@ -16,74 +20,87 @@ def __init__(self, config: BaseConfig, args): self.config: BaseConfig = config self.cz = factory.commiter_factory(self.config) - self.file_name = args["file_name"] or self.config.settings.get("changelog_file") + self.start_rev = args.get("start_rev") + self.file_name = args.get("file_name") or self.config.settings.get( + "changelog_file" + ) self.incremental = args["incremental"] self.dry_run = args["dry_run"] - self.start_rev = args["start_rev"] + self.unreleased_version = args["unreleased_version"] + + def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str: + """Try to find the 'start_rev'. + + We use a similarity approach. We know how to parse the version from the markdown + changelog, but not the whole tag, we don't even know how's the tag made. + + This 'smart' function tries to find a similarity between the found version number + and the available tag. + + The SIMILARITY_THRESHOLD is an empirical value, it may have to be adjusted based + on our experience. + """ + SIMILARITY_THRESHOLD = 0.89 + tag_ratio = map( + lambda tag: (SequenceMatcher(None, latest_version, tag.name).ratio(), tag), + tags, + ) + try: + score, tag = max(tag_ratio, key=itemgetter(0)) + except ValueError: + raise SystemExit(NO_REVISION) + if score < SIMILARITY_THRESHOLD: + raise SystemExit(NO_REVISION) + start_rev = tag.name + return start_rev def __call__(self): - # changelog_map = self.cz.changelog_map commit_parser = self.cz.commit_parser changelog_pattern = self.cz.changelog_pattern + start_rev = self.start_rev + unreleased_version = self.unreleased_version + changelog_meta: Dict = {} if not changelog_pattern or not commit_parser: out.error( f"'{self.config.settings['name']}' rule does not support changelog" ) raise SystemExit(NO_PATTERN_MAP) - # pat = re.compile(changelog_pattern) - - commits = git.get_commits(start=self.start_rev) - if not commits: - out.error("No commits found") - raise SystemExit(NO_COMMITS_FOUND) tags = git.get_tags() if not tags: tags = [] + if self.incremental: + changelog_meta = changelog.get_metadata(self.file_name) + latest_version = changelog_meta.get("latest_version") + if latest_version: + start_rev = self._find_incremental_rev(latest_version, tags) + + commits = git.get_commits(start=start_rev) + if not commits: + out.error("No commits found") + raise SystemExit(NO_COMMITS_FOUND) + tree = changelog.generate_tree_from_commits( - commits, tags, commit_parser, changelog_pattern + commits, tags, commit_parser, changelog_pattern, unreleased_version ) changelog_out = changelog.render_changelog(tree) - # tag_map = {tag.rev: tag.name for tag in git.get_tags()} - - # entries = OrderedDict() - # # The latest commit is not tagged - # latest_commit = commits[0] - # if latest_commit.rev not in tag_map: - # current_key = "Unreleased" - # entries[current_key] = OrderedDict( - # {value: [] for value in changelog_map.values()} - # ) - # else: - # current_key = tag_map[latest_commit.rev] - - # for commit in commits: - # if commit.rev in tag_map: - # current_key = tag_map[commit.rev] - # entries[current_key] = OrderedDict( - # {value: [] for value in changelog_map.values()} - # ) - - # matches = pat.match(commit.message) - # if not matches: - # continue - - # processed_commit = self.cz.process_commit(commit.message) - # for group_name, commit_type in changelog_map.items(): - # if matches.group(group_name): - # entries[current_key][commit_type].append(processed_commit) - # break - - # template_file = pkg_resources.resource_string( - # __name__, "../templates/keep_a_changelog_template.j2" - # ).decode("utf-8") - # jinja_template = Template(template_file) - # changelog_str = jinja_template.render(entries=entries) + if self.dry_run: out.write(changelog_out) raise SystemExit(0) + lines = [] + if self.incremental and os.path.isfile(self.file_name): + with open(self.file_name, "r") as changelog_file: + lines = changelog_file.readlines() + with open(self.file_name, "w") as changelog_file: - changelog_file.write(changelog_out) + if self.incremental: + new_lines = changelog.incremental_build( + changelog_out, lines, changelog_meta + ) + changelog_file.writelines(new_lines) + else: + changelog_file.write(changelog_out) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index b428d696cd..ac73cc295b 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -32,7 +32,9 @@ def __call__(self): values_to_add["version"] = Version(tag).public values_to_add["tag_format"] = self._ask_tag_format(tag) self._update_config_file(values_to_add) - out.write("The configuration are all set.") + out.write("You can bump the version and create cangelog running:\n") + out.info("cz bump --changelog") + out.success("The configuration are all set.") else: # TODO: handle the case that config file exist but no value out.line(f"Config file {self.config.path} already exists") diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 070b415646..1f4adf52e6 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -34,3 +34,4 @@ bump_message = "bump: version $current_version → $new_version" commit_parser = r"^(?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?:\s(?P<message>.*)?" # noqa +version_parser = r"(?P<version>([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?)" diff --git a/commitizen/error_codes.py b/commitizen/error_codes.py index 20cc54aa9e..9095399c3f 100644 --- a/commitizen/error_codes.py +++ b/commitizen/error_codes.py @@ -22,3 +22,6 @@ # Check NO_COMMIT_MSG = 13 INVALID_COMMIT_MSG = 14 + +# Changelog +NO_REVISION = 16 diff --git a/commitizen/git.py b/commitizen/git.py index feac0697b0..5c3b5926ee 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -15,6 +15,8 @@ class GitProtocol(Protocol): class GitObject: rev: str + name: str + date: str def __eq__(self, other) -> bool: if not hasattr(other, "rev"): diff --git a/docs/changelog.md b/docs/changelog.md index db8d0bc6ed..b5f4fece2c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -14,6 +14,16 @@ cz changelog cz ch ``` +It has support for incremental changelog: + +- Build from latest version found in changelog, this is useful if you have a different changelog and want to use chommitizen +- Update unreleased area +- Allows users to manually touch the changelog without being rewritten. + +## Constrains + +At the moment this features is constrained only to markdown files. + ## Description These are the variables used by the changelog generator. @@ -26,7 +36,7 @@ These are the variables used by the changelog generator. - **<scope>**: <message> ``` -It will create a full of the above block per version found in the tags. +It will create a full block like above per version found in the tags. And it will create a list of the commits found. The `change_type` and the `scope` are optional, they don't need to be provided, but if your regex does they will be rendered. @@ -45,5 +55,10 @@ and the following variables are expected: - **required**: is the only one required to be parsed by the regex +## 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. + [keepachangelog]: https://keepachangelog.com/ [semver]: https://semver.org/ diff --git a/pyproject.toml b/pyproject.toml index 7a2685a783..11306bb4bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ mypy = "^0.770" mkdocs = "^1.0" mkdocs-material = "^4.1" isort = "^4.3.21" +freezegun = "^0.3.15" [tool.poetry.scripts] cz = "commitizen.cli:main" diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index 981cfa654c..77bb74b522 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -1,8 +1,10 @@ +import os import sys +from datetime import date import pytest -from commitizen import cli +from commitizen import cli, git from tests.utils import create_file_and_commit @@ -15,26 +17,6 @@ def test_changlog_on_empty_project(mocker): cli.main() -@pytest.mark.usefixtures("tmp_commitizen_project") -def test_changlog_from_start(mocker, capsys): - create_file_and_commit("feat: new file") - create_file_and_commit("refactor: not in changelog") - create_file_and_commit("Merge into master") - - testargs = ["cz", "changelog", "--dry-run"] - mocker.patch.object(sys, "argv", testargs) - - with pytest.raises(SystemExit): - cli.main() - - out, _ = capsys.readouterr() - - assert ( - out - == "\n## Unreleased \n\n### Refactor\n\n- not in changelog\n\n### Feat\n\n- new file\n\n" - ) - - @pytest.mark.usefixtures("tmp_commitizen_project") def test_changlog_from_version_zero_point_two(mocker, capsys): create_file_and_commit("feat: new file") @@ -67,3 +49,182 @@ def test_changlog_with_unsupported_cz(mocker, capsys): cli.main() out, err = capsys.readouterr() assert "'cz_jira' rule does not support changelog" in err + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_from_start(mocker, capsys): + changelog_path = os.path.join(os.getcwd(), "CHANGELOG.md") + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: is in changelog") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, "r") as f: + out = f.read() + + assert ( + out + == "\n## Unreleased \n\n### Refactor\n\n- is in changelog\n\n### Feat\n\n- new file\n" + ) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_replacing_unreleased_using_incremental(mocker, capsys): + changelog_path = os.path.join(os.getcwd(), "CHANGELOG.md") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, "r") as f: + out = f.read() + + today = date.today().isoformat() + assert ( + out + == f"\n\n## Unreleased \n\n### Feat\n\n- add more stuff\n\n### Fix\n\n- mama gotta work\n\n## 0.2.0 ({today})\n\n### Fix\n\n- output glitch\n\n### Feat\n\n- add new output\n" + ) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_is_persisted_using_incremental(mocker, capsys): + changelog_path = os.path.join(os.getcwd(), "CHANGELOG.md") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("Merge into master") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, "a") as f: + f.write("\nnote: this should be persisted using increment\n") + + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, "r") as f: + out = f.read() + + today = date.today().isoformat() + assert ( + out + == f"\n\n## Unreleased \n\n### Feat\n\n- add more stuff\n\n### Fix\n\n- mama gotta work\n\n## 0.2.0 ({today})\n\n### Fix\n\n- output glitch\n\n### Feat\n\n- add new output\n\nnote: this should be persisted using increment\n" + ) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_incremental_angular_sample(mocker, capsys): + changelog_path = os.path.join(os.getcwd(), "CHANGELOG.md") + with open(changelog_path, "w") as f: + f.write( + "# [10.0.0-next.3](https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3) (2020-04-22)\n" + "\n" + "### Bug Fixes" + "\n" + "* **common:** format day-periods that cross midnight ([#36611](https://github.com/angular/angular/issues/36611)) ([c6e5fc4](https://github.com/angular/angular/commit/c6e5fc4)), closes [#36566](https://github.com/angular/angular/issues/36566)\n" + ) + create_file_and_commit("irrelevant commit") + git.tag("10.0.0-next.3") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, "r") as f: + out = f.read() + + assert ( + out + == "\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# [10.0.0-next.3](https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3) (2020-04-22)\n\n### Bug Fixes\n* **common:** format day-periods that cross midnight ([#36611](https://github.com/angular/angular/issues/36611)) ([c6e5fc4](https://github.com/angular/angular/commit/c6e5fc4)), closes [#36566](https://github.com/angular/angular/issues/36566)\n" + ) + + +KEEP_A_CHANGELOG = """# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). +""" + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changlog_incremental_keep_a_changelog_sample(mocker, capsys): + changelog_path = os.path.join(os.getcwd(), "CHANGELOG.md") + with open(changelog_path, "w") as f: + f.write(KEEP_A_CHANGELOG) + create_file_and_commit("irrelevant commit") + git.tag("1.0.0") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, "r") as f: + out = f.read() + + assert ( + 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""" + ) diff --git a/tests/test_changelog_meta.py b/tests/test_changelog_meta.py new file mode 100644 index 0000000000..505daefb83 --- /dev/null +++ b/tests/test_changelog_meta.py @@ -0,0 +1,165 @@ +import os + +import pytest + +from commitizen import changelog + +CHANGELOG_A = """ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +- Start using "changelog" over "change log" since it's the common usage. + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. +""".strip() + +CHANGELOG_B = """ +## [Unreleased] +- Start using "changelog" over "change log" since it's the common usage. + +## 1.2.0 +""".strip() + +CHANGELOG_C = """ +# Unreleased + +## v1.0.0 +""" + +CHANGELOG_D = """ +## Unreleased +- Start using "changelog" over "change log" since it's the common usage. +""" + + +@pytest.fixture +def changelog_a_file(): + changelog_path = "tests/CHANGELOG.md" + + with open(changelog_path, "w") as f: + f.write(CHANGELOG_A) + + yield changelog_path + + os.remove(changelog_path) + + +@pytest.fixture +def changelog_b_file(): + changelog_path = "tests/CHANGELOG.md" + + with open(changelog_path, "w") as f: + f.write(CHANGELOG_B) + + yield changelog_path + + os.remove(changelog_path) + + +@pytest.fixture +def changelog_c_file(): + changelog_path = "tests/CHANGELOG.md" + + with open(changelog_path, "w") as f: + f.write(CHANGELOG_C) + + yield changelog_path + + os.remove(changelog_path) + + +@pytest.fixture +def changelog_d_file(): + changelog_path = "tests/CHANGELOG.md" + + with open(changelog_path, "w") as f: + f.write(CHANGELOG_D) + + yield changelog_path + + os.remove(changelog_path) + + +VERSIONS_EXAMPLES = [ + ("## [1.0.0] - 2017-06-20", "1.0.0"), + ( + "# [10.0.0-next.3](https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3) (2020-04-22)", + "10.0.0-next.3", + ), + ("### 0.19.1 (Jan 7, 2020)", "0.19.1"), + ("## 1.0.0", "1.0.0"), + ("## v1.0.0", "1.0.0"), + ("## v1.0.0 - (2012-24-32)", "1.0.0"), + ("# version 2020.03.24", "2020.03.24"), + ("## [Unreleased]", None), + ("All notable changes to this project will be documented in this file.", None), + ("# Changelog", None), + ("### Bug Fixes", None), +] + + +@pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) +def test_changelog_detect_version(line_from_changelog, output_version): + version = changelog.parse_version_from_markdown(line_from_changelog) + assert version == output_version + + +TITLES_EXAMPLES = [ + ("## [1.0.0] - 2017-06-20", "##"), + ("## [Unreleased]", "##"), + ("# Unreleased", "#"), +] + + +@pytest.mark.parametrize("line_from_changelog,output_title", TITLES_EXAMPLES) +def test_parse_title_type_of_line(line_from_changelog, output_title): + title = changelog.parse_title_type_of_line(line_from_changelog) + assert title == output_title + + +def test_get_metadata_from_a(changelog_a_file): + meta = changelog.get_metadata(changelog_a_file) + assert meta == { + "latest_version": "1.0.0", + "latest_version_position": 10, + "unreleased_end": 10, + "unreleased_start": 7, + } + + +def test_get_metadata_from_b(changelog_b_file): + meta = changelog.get_metadata(changelog_b_file) + assert meta == { + "latest_version": "1.2.0", + "latest_version_position": 3, + "unreleased_end": 3, + "unreleased_start": 0, + } + + +def test_get_metadata_from_c(changelog_c_file): + meta = changelog.get_metadata(changelog_c_file) + assert meta == { + "latest_version": "1.0.0", + "latest_version_position": 3, + "unreleased_end": 3, + "unreleased_start": 1, + } + + +def test_get_metadata_from_d(changelog_d_file): + meta = changelog.get_metadata(changelog_d_file) + assert meta == { + "latest_version": None, + "latest_version_position": None, + "unreleased_end": 2, + "unreleased_start": 1, + } diff --git a/tests/test_changelog_parser.py b/tests/test_changelog_parser.py index 352447ae26..f07d8c3f63 100644 --- a/tests/test_changelog_parser.py +++ b/tests/test_changelog_parser.py @@ -33,7 +33,7 @@ def changelog_content() -> str: @pytest.fixture -def existing_changelog_file(request): +def existing_changelog_file(): changelog_path = "tests/CHANGELOG.md" with open(changelog_path, "w") as f: From 7cfb4e5312f5df977e487102df21213bf274e761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= <santiwilly@gmail.com> Date: Sat, 2 May 2020 12:33:46 +0200 Subject: [PATCH 31/35] feat(changelog): add support for any commit rule system --- commitizen/cz/base.py | 10 +++++++--- commitizen/templates/keep_a_changelog_template.j2 | 2 +- tests/commands/test_changelog_command.py | 12 +++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index 207be89d1d..c6e74d95b0 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -9,8 +9,6 @@ class BaseCommitizen(metaclass=ABCMeta): bump_pattern: Optional[str] = None bump_map: Optional[dict] = None - changelog_pattern: Optional[str] = None - changelog_map: Optional[dict] = None default_style_config: List[Tuple[str, str]] = [ ("qmark", "fg:#ff9d00 bold"), ("question", "bold"), @@ -23,7 +21,13 @@ class BaseCommitizen(metaclass=ABCMeta): ("text", ""), ("disabled", "fg:#858585 italic"), ] - commit_parser: Optional[str] = None + + # The whole subject will be parsed as message by default + # This allows supporting changelog for any rule system. + # It can be modified per rule + commit_parser: Optional[str] = r"(?P<message>.*)" + changelog_pattern: Optional[str] = r".*" + changelog_map: Optional[dict] = None # TODO: Use it def __init__(self, config: BaseConfig): self.config = config diff --git a/commitizen/templates/keep_a_changelog_template.j2 b/commitizen/templates/keep_a_changelog_template.j2 index a0fadb0223..be0d0d26e6 100644 --- a/commitizen/templates/keep_a_changelog_template.j2 +++ b/commitizen/templates/keep_a_changelog_template.j2 @@ -11,7 +11,7 @@ {% for change in changes %} {% if change.scope %} - **{{ change.scope }}**: {{ change.message }} -{% else %} +{% elif change.message %} - {{ change.message }} {% endif %} {% endfor %} diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index 77bb74b522..d78d04bee2 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -41,14 +41,20 @@ def test_changlog_from_version_zero_point_two(mocker, capsys): @pytest.mark.usefixtures("tmp_commitizen_project") -def test_changlog_with_unsupported_cz(mocker, capsys): +def test_changlog_with_different_cz(mocker, capsys): + create_file_and_commit("JRA-34 #comment corrected indent issue") + create_file_and_commit("JRA-35 #time 1w 2d 4h 30m Total work logged") + testargs = ["cz", "-n", "cz_jira", "changelog", "--dry-run"] mocker.patch.object(sys, "argv", testargs) with pytest.raises(SystemExit): cli.main() - out, err = capsys.readouterr() - assert "'cz_jira' rule does not support changelog" in err + out, _ = capsys.readouterr() + assert ( + out + == "\n## Unreleased \n\n\n- JRA-35 #time 1w 2d 4h 30m Total work logged\n- JRA-34 #comment corrected indent issue\n\n" + ) @pytest.mark.usefixtures("tmp_commitizen_project") From 9b407991b291cbbf6489fd0873e2bbd5eb6c3c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= <santiwilly@gmail.com> Date: Sat, 2 May 2020 12:44:12 +0200 Subject: [PATCH 32/35] docs(changelog): update information about using changelog command --- docs/changelog.md | 85 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index b5f4fece2c..fab38ea2ad 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,7 +4,28 @@ This command will generate a changelog following the commiting rules established ## Usage -In the command line run +```bash +$ cz changelog --help +usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] + [--unreleased-version UNRELEASED_VERSION] [--incremental] + [--start-rev START_REV] + +optional arguments: + -h, --help show this help message and exit + --dry-run show changelog to stdout + --file-name FILE_NAME + file name of changelog (default: 'CHANGELOG.md') + --unreleased-version UNRELEASED_VERSION + set the value for the new version (use the tag value), + instead of using unreleased + --incremental generates changelog from last created version, useful + if the changelog has been manually modified + --start-rev START_REV + start rev of the changelog.If not set, it will + generate changelog from the start +``` + +### Examples ```bash cz changelog @@ -14,15 +35,9 @@ cz changelog cz ch ``` -It has support for incremental changelog: - -- Build from latest version found in changelog, this is useful if you have a different changelog and want to use chommitizen -- Update unreleased area -- Allows users to manually touch the changelog without being rewritten. - ## Constrains -At the moment this features is constrained only to markdown files. +changelog generation is constrained only to **markdown** files. ## Description @@ -55,6 +70,60 @@ and the following variables are expected: - **required**: is the only one required to be parsed by the regex +## Configuration + +### `unreleased_version` + +There is usually an egg and chicken situation when automatically +bumping the version and creating the changelog. +If you bump the version first, you have no changelog, you have to +create it later, and it won't be included in +the release of the created version. + +If you create the changelog before bumping the version, then you +usually don't have the latest tag, and the _Unreleased_ title appears. + +By introducing `unreleased_version` you can prevent this situation. + +Before bumping you can run: + +```bash +cz changelog --unreleased_version="v1.0.0" +``` + +Remember to use the tag instead of the raw version number + +For example if the format of your tag includes a `v` (`v1.0.0`), then you should use that, +if your tag is the same as the raw version, then ignore this. + +Alternatively you can directly bump the version and create the changelog by doing + +```bash +cz bump --changelog +``` + +### `file-name` + +This value can be updated in the `toml` file with the key `changelog_file` under `tools.commitizen` + +Specify the name of the output file, remember that changelog only works with markdown. + +```bash +cz changelog --file-name="CHANGES.md" +``` + +### `incremental` + +Benefits: + +- Build from latest version found in changelog, this is useful if you have a different changelog and want to use commitizen +- Update unreleased area +- Allows users to manually touch the changelog without being rewritten. + +```bash +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 From 69402415f2b3012bf163ce9d03c6e9152def2bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= <santiwilly@gmail.com> Date: Sat, 2 May 2020 12:45:58 +0200 Subject: [PATCH 33/35] docs(bump): add information about changelog --- docs/bump.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/bump.md b/docs/bump.md index 393baefd15..d9527135e4 100644 --- a/docs/bump.md +++ b/docs/bump.md @@ -54,7 +54,7 @@ Some examples: ```bash $ cz bump --help -usage: cz bump [-h] [--dry-run] [--files-only] [--yes] +usage: cz bump [-h] [--dry-run] [--files-only] [--changelog] [--yes] [--tag-format TAG_FORMAT] [--bump-message BUMP_MESSAGE] [--prerelease {alpha,beta,rc}] [--increment {MAJOR,MINOR,PATCH}] @@ -63,10 +63,11 @@ optional arguments: -h, --help show this help message and exit --dry-run show output to stdout, no commit, no modified files --files-only bump version in the files from the config + --changelog, -ch generate the changelog for the newest version --yes accept automatically questions done --tag-format TAG_FORMAT - the format used to tag the commit and read it, use it in - existing projects, wrap around simple quotes + the format used to tag the commit and read it, use it + in existing projects, wrap around simple quotes --bump-message BUMP_MESSAGE template used to create the release commmit, useful when working with CI @@ -78,6 +79,14 @@ optional arguments: ## Configuration +### `changelog` + +Generate a **changelog** along with the new version and tag when bumping. + +```bash +cz bump --changelog +``` + ### `tag_format` It is used to read the format from the git tags, and also to generate the tags. From 5fec1d48bfd67622a6713ed3e4595f3451c7d325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= <santiwilly@gmail.com> Date: Sat, 2 May 2020 16:04:47 +0200 Subject: [PATCH 34/35] ci(bumpversion): generate changelog along with the version --- .github/workflows/bumpversion.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bumpversion.yml b/.github/workflows/bumpversion.yml index f7559b420b..90c8af6373 100644 --- a/.github/workflows/bumpversion.yml +++ b/.github/workflows/bumpversion.yml @@ -32,7 +32,7 @@ jobs: git pull origin master --tags - name: Create bump run: | - cz bump --yes + cz bump --yes --changelog git tag - name: Push changes uses: Woile/github-push-action@master From aca2fe929f3321af8e92bd5197f0fbc83e0ea23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Fraire=20Willemo=C3=ABs?= <santiwilly@gmail.com> Date: Sat, 2 May 2020 16:07:15 +0200 Subject: [PATCH 35/35] fix(changelog): check get_metadata for existing changelog file --- commitizen/changelog.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 9596dcb027..3990b4cfb5 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -25,6 +25,7 @@ - [ ] hook after changelog is generated (api calls) - [ ] add support for change_type maps """ +import os import re from collections import defaultdict from typing import Dict, Iterable, List, Optional @@ -157,6 +158,14 @@ def get_metadata(filepath: str) -> Dict: unreleased_title: Optional[str] = None latest_version: Optional[str] = None latest_version_position: Optional[int] = None + if not os.path.isfile(filepath): + return { + "unreleased_start": None, + "unreleased_end": None, + "latest_version": None, + "latest_version_position": None, + } + with open(filepath, "r") as changelog_file: for index, line in enumerate(changelog_file): line = line.strip().lower()