From 6429d4e9a5d4f311b6fe86e63c27abd21dc82097 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/26] feat(changelog): changelog tree generation from markdown --- commitizen/changelog.py | 133 +++++++++++++++++++++ commitizen/cli.py | 5 + commitizen/commands/__init__.py | 2 + commitizen/commands/changelog.py | 14 +++ tests/test_changelog.py | 199 +++++++++++++++++++++++++++++++ 5 files changed, 353 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 f7e9f28687..aa5e793918 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 e315a987c9..0475f65014 100644 --- a/commitizen/commands/__init__.py +++ b/commitizen/commands/__init__.py @@ -7,12 +7,14 @@ 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 546828e76e381a80c7324cfa5925b8523537b5bf Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 15 Jan 2020 19:42:22 +0800 Subject: [PATCH 02/26] 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 258f43ca5e..5c2076fdc0 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -57,3 +57,10 @@ def schema_pattern(self) -> str: def info(self) -> 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 705dc2f1e92d265b86596eeef2bd27bc027ff4fd Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 15 Jan 2020 19:43:05 +0800 Subject: [PATCH 03/26] feat(cz/conventinal_commits): add changelog_map, changelog_pattern and implement process_commit --- .../conventional_commits.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index c67b2a5a0d..f2b5d34aaf 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 commitizen import defaults from commitizen.cz.base import BaseCommitizen @@ -28,6 +30,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: questions = [ @@ -170,11 +176,10 @@ def schema(self) -> str: ) def schema_pattern(self) -> str: - PATTERN = ( - r"(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)" - r"(\([\w\-]+\))?:\s.*" + return ( + r"(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert)" + r"(\(.+\))?:(\s.*)" ) - return PATTERN def info(self) -> str: dir_path = os.path.dirname(os.path.realpath(__file__)) @@ -182,3 +187,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 d104367256e64ebd8ec913a5ff7805c76cd23396 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 15 Jan 2020 19:44:24 +0800 Subject: [PATCH 04/26] 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 10a0f3da216314c01a07e8476d4d09b1abe8c20a Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 17 Jan 2020 16:31:04 +0800 Subject: [PATCH 05/26] 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 9ea446ce41ba9de6a436c4c9a01fde57051a3943 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Wed, 15 Jan 2020 16:11:31 +0800 Subject: [PATCH 06/26] 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 0475f65014..b85ab40269 100644 --- a/commitizen/commands/__init__.py +++ b/commitizen/commands/__init__.py @@ -14,7 +14,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 4e58392763884b48febba3d391b15a8dee4775a8 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 23 Jan 2020 15:57:18 +0800 Subject: [PATCH 07/26] 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 f2b5d34aaf..f4874fb511 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -30,10 +30,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: questions = [ diff --git a/pyproject.toml b/pyproject.toml index 7398fe6f9e..773920443c 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 741e66de44a2cd7eadcbc00df95d12af6425356b Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 21:35:38 +0800 Subject: [PATCH 08/26] 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 aa5e793918..b3c0db1cfa 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 e831c3302225dcd435a4b410e77a2487b48f98a2 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 21:36:27 +0800 Subject: [PATCH 09/26] 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 b3c0db1cfa..ba7c91fbe3 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 351a67457971087cd8ad8ae3c6953a0e37763b89 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 21:48:52 +0800 Subject: [PATCH 10/26] 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 ba7c91fbe3..d5066a5b87 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 90f93fa802ca3c2472ca452243ba79cf8e845953 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 21:53:17 +0800 Subject: [PATCH 11/26] 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 ca742225d1af49ea98e1edbe18dd70034b602b80 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 22:18:28 +0800 Subject: [PATCH 12/26] 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 bb647f9fc539406f1c46905e6c57d6f7799ac01c Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 22:24:45 +0800 Subject: [PATCH 13/26] 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 f95ad3987d822a8f3db0098ec4ae00a9a92726f3 Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 22:42:25 +0800 Subject: [PATCH 14/26] 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 d5066a5b87..d983ed7468 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 a8827368db7df8c77038b873b5e629ac0f24666b Mon Sep 17 00:00:00 2001 From: LeeW Date: Thu, 23 Jan 2020 22:47:13 +0800 Subject: [PATCH 15/26] 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 5c2076fdc0..6fb53b22bd 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -7,6 +7,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 e5b50fe02e7f6a8cb1b7a28fbd1d5fbc071b92e9 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 23 Jan 2020 23:17:15 +0800 Subject: [PATCH 16/26] 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 4232abac895bf823f0424b5359a7e3e5b2b03000 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 23 Jan 2020 23:17:47 +0800 Subject: [PATCH 17/26] 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 9b91fadc246871920a54facd080eee829776db73 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Thu, 23 Jan 2020 23:40:20 +0800 Subject: [PATCH 18/26] 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 07ff25c8074e204529f1505a484999e3d43b3891 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 08:08:12 +0800 Subject: [PATCH 19/26] 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 d983ed7468..e9139b5b91 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", }, ], }, @@ -189,9 +163,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 c4177505c16548795c88756ab3e52ddda472c91c Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 11:37:16 +0800 Subject: [PATCH 20/26] docs(README): add changelog command --- README.rst | 191 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/README.md | 23 +++--- 2 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000000..1779fbac34 --- /dev/null +++ b/README.rst @@ -0,0 +1,191 @@ +============= +Commitizen +============= + + Python 3 command-line utility to standardize commit messages and bump version + + +.. image:: https://github.com/Woile/commitizen/workflows/Python%20package/badge.svg + :alt: Github Actions + :target: https://github.com/Woile/commitizen/actions + +.. image:: https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square + :alt: Conventional Commits + :target: https://conventionalcommits.org + +.. image:: https://img.shields.io/pypi/v/commitizen.svg?style=flat-square + :alt: PyPI Package latest release + :target: https://pypi.org/project/commitizen/ + +.. image:: https://img.shields.io/pypi/pyversions/commitizen.svg?style=flat-square + :alt: Supported versions + :target: https://pypi.org/project/commitizen/ + +.. image:: https://img.shields.io/codecov/c/github/Woile/commitizen.svg?style=flat-square + :alt: Codecov + :target: https://codecov.io/gh/Woile/commitizen + +.. image:: docs/images/demo.gif + :alt: Example running commitizen + +-------------- + +**Documentation**: https://Woile.github.io/commitizen/ + +-------------- + +.. contents:: + :depth: 2 + + +About +========== + +Commitizen is a tool designed for teams. + +Its main purpose is to define a standard way of committing rules +and communicating it (using the cli provided by commitizen). + +The reasoning behind it is that it is easier to read, and enforces writing +descriptive commits. + +Besides that, having a convention on your commits makes it possible to +parse them and use them for something else, like generating automatically +the version or a changelog. + + +Installation +============= + +:: + + pip install -U commitizen + +:: + + poetry add commitizen --dev + + +**Global installation** + +:: + + sudo pip3 install -U commitizen + +Features +======== + +- Command-line utility to create commits with your rules. Defaults: `conventional commits`_ +- Display information about your commit rules (commands: schema, example, info) +- Bump version automatically using semantic verisoning based on the commits. `Read More <./docs/bump.md>`_ +- Generate a changelog using "Keep a changelog" (Planned feature) + + +Commit rules +============ + +This client tool prompts the user with information about the commit. + +Based on `conventional commits`_ + +This is an example of how the git messages history would look like: + +:: + + fix: minor typos in code + feat: new command update + docs: improved commitizens tab in readme + feat(cz): jira smart commits + refactor(cli): renamed all to ls command + feat: info command for angular + docs(README): added badges + docs(README): added about, installation, creating, etc + feat(config): new loads from ~/.cz and working project .cz .cz.cfg and setup.cfg + +And then, by using ``cz bump`` , you can change the version of your project. + +``feat`` to ``MINOR`` +``fix`` to ``PATCH`` + + +Commitizens +=========== + +These are the available committing styles by default: + +* cz_conventional_commits: `conventional commits`_ +* cz_jira: `jira smart commits `_ + + +The installed ones can be checked with: + +:: + + cz ls + + + +Commiting +========= + +Run in your terminal + +:: + + cz commit + +or the shortcut + +:: + + cz c + + +Usage +===== + +:: + + $ cz --help + usage: cz [-h] [--debug] [-n NAME] [--version] + {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 (default: + cz_conventional_commits) + --version get the version of the installed commitizen + + commands: + {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) + +Contributing +============ + +Feel free to create a PR. + +1. Clone the repo. +2. Add your modifications +3. Create a virtualenv +4. Run :code:`./scripts/test` + + +.. _conventional commits: https://conventionalcommits.org/ diff --git a/docs/README.md b/docs/README.md index 561d9487e1..affc06df94 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 ``` ## Contributing From dbe926dceef01583eb8bacaeb63d99f577da4669 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 11:38:22 +0800 Subject: [PATCH 21/26] 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 4f3a34bb77db99d62689701f848b87de2edbd8ea Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 11:38:46 +0800 Subject: [PATCH 22/26] 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 947d7650889fc306894882b726835b0138a06978 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 11:39:54 +0800 Subject: [PATCH 23/26] 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 6f72b6a322501b98106181290c9943b3baa68916 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 16:36:17 +0800 Subject: [PATCH 24/26] 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 e9139b5b91..e463d258ae 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 a9358f2b10..0b4a06452c 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -8,6 +8,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 c7527cef79..b07bfe5a7a 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -45,6 +45,7 @@ "bump_message": None, "version_files": ["commitizen/__version__.py", "pyproject.toml"], "style": [["pointer", "reverse"], ["question", "underline"]], + "changelog_file": "CHANGELOG.md", } _new_settings = { @@ -54,6 +55,7 @@ "bump_message": None, "version_files": ["commitizen/__version__.py", "pyproject.toml"], "style": [["pointer", "reverse"], ["question", "underline"]], + "changelog_file": "CHANGELOG.md", } _read_settings = { @@ -61,6 +63,7 @@ "version": "1.0.0", "version_files": ["commitizen/__version__.py", "pyproject.toml"], "style": [["pointer", "reverse"], ["question", "underline"]], + "changelog_file": "CHANGELOG.md", } From 3bdd9b2234fe67a9993bb4298657ff6304aedc63 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 24 Jan 2020 16:38:08 +0800 Subject: [PATCH 25/26] docs(config): add changlog_file a config option --- docs/config.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/config.md b/docs/config.md index 88f4850d45..d7a1f72ca5 100644 --- a/docs/config.md +++ b/docs/config.md @@ -70,5 +70,6 @@ The extra tab before the square brackets (`]`) at the end is required. | `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://woile.github.io/commitizen/customization/) | From 202121ffdebca6ddbb44a7febaeec1c9dc1ac45c Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Sun, 1 Mar 2020 18:43:14 +0800 Subject: [PATCH 26/26] fix(conventional_commits): fix checking regular expression --- 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 f4874fb511..10c5ef0f75 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -177,7 +177,7 @@ def schema(self) -> str: def schema_pattern(self) -> str: return ( - r"(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert)" + r"(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)" r"(\(.+\))?:(\s.*)" )