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/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..e463d258ae 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -34,9 +34,9 @@ # "required": True, "commands": [ { - "name": "ls", - "help": "show available commitizens", - "func": commands.ListCz, + "name": ["init"], + "help": "init commitizen configuration", + "func": commands.Init, }, { "name": ["commit", "c"], @@ -55,6 +55,11 @@ }, ], }, + { + "name": "ls", + "help": "show available commitizens", + "func": commands.ListCz, + }, { "name": "example", "help": "show commit example", @@ -114,33 +119,29 @@ ], }, { - "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", + "help": "file name of changelog (default: 'CHANGELOG.md')", }, { - "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", }, ], }, @@ -161,9 +162,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", + }, + ], }, ], }, diff --git a/commitizen/commands/__init__.py b/commitizen/commands/__init__.py index e315a987c9..b85ab40269 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..280f1b8f8c --- /dev/null +++ b/commitizen/commands/changelog.py @@ -0,0 +1,79 @@ +import re +import pkg_resources +from collections import OrderedDict + +from jinja2 import Template + +from commitizen import factory, out, git +from commitizen.config import BaseConfig +from commitizen.error_codes import NO_COMMITS_FOUND, NO_PATTERN_MAP + + +class Changelog: + """Generate a changelog based on the commit history.""" + + 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.dry_run = args["dry_run"] + self.start_rev = args["start_rev"] + + def __call__(self): + changelog_map = self.cz.changelog_map + changelog_pattern = self.cz.changelog_pattern + if not changelog_map or not changelog_pattern: + 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()} + + 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) + raise SystemExit(0) + + with open(self.file_name, "w") as changelog_file: + changelog_file.write(changelog_str) diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index 258f43ca5e..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"), @@ -57,3 +59,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] diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index c67b2a5a0d..10c5ef0f75 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"(?P.*\n\nBREAKING CHANGE)|(?P^feat)|(?P^fix)" + ) + changelog_map = OrderedDict({"break": "breaking", "feat": "feat", "fix": "fix"}) def questions(self) -> list: questions = [ @@ -170,11 +176,10 @@ def schema(self) -> str: ) def schema_pattern(self) -> str: - PATTERN = ( + return ( r"(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)" - r"(\([\w\-]+\))?:\s.*" + 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() 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/commitizen/templates/keep_a_changelog_template.j2 b/commitizen/templates/keep_a_changelog_template.j2 new file mode 100644 index 0000000000..d3fba14668 --- /dev/null +++ b/commitizen/templates/keep_a_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/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 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/) | 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" 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/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py new file mode 100644 index 0000000000..a053e6c238 --- /dev/null +++ b/tests/commands/test_changelog_command.py @@ -0,0 +1,68 @@ +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 diff --git a/tests/test_changelog.py b/tests/test_changelog.py new file mode 100644 index 0000000000..ba0a730859 --- /dev/null +++ b/tests/test_changelog.py @@ -0,0 +1,216 @@ +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", + }, + ] 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", } 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)