import re from dataclasses import dataclass from pathlib import Path from typing import Any, Optional import pytest from jinja2 import FileSystemLoader from commitizen import changelog, git from commitizen.changelog_formats import ChangelogFormat from commitizen.cz.conventional_commits.conventional_commits import ( ConventionalCommitsCz, ) from commitizen.exceptions import InvalidConfigurationError from commitizen.version_schemes import Pep440 COMMITS_DATA: list[dict[str, Any]] = [ { "rev": "141ee441c9c9da0809c554103a558eb17c30ed17", "parents": ["6c4948501031b7d6405b54b21d3d635827f9421b"], "title": "bump: version 1.1.1 → 1.2.0", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "6c4948501031b7d6405b54b21d3d635827f9421b", "parents": ["ddd220ad515502200fe2dde443614c1075d26238"], "title": "docs: how to create custom bumps", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "ddd220ad515502200fe2dde443614c1075d26238", "parents": ["ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc"], "title": "feat: custom cz plugins now support bumping version", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc", "parents": ["56c8a8da84e42b526bcbe130bd194306f7c7e813"], "title": "docs: added bump gif", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "56c8a8da84e42b526bcbe130bd194306f7c7e813", "parents": ["74c6134b1b2e6bb8b07ed53410faabe99b204f36"], "title": "bump: version 1.1.0 → 1.1.1", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "74c6134b1b2e6bb8b07ed53410faabe99b204f36", "parents": ["cbc7b5f22c4e74deff4bc92d14e19bd93524711e"], "title": "refactor: changed stdout statements", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "cbc7b5f22c4e74deff4bc92d14e19bd93524711e", "parents": ["1ba46f2a63cb9d6e7472eaece21528c8cd28b118"], "title": "fix(bump): commit message now fits better with semver", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "1ba46f2a63cb9d6e7472eaece21528c8cd28b118", "parents": ["c35dbffd1bb98bb0b3d1593797e79d1c3366af8f"], "title": "fix: conventional commit 'breaking change' in body instead of title", "body": "closes #16", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "c35dbffd1bb98bb0b3d1593797e79d1c3366af8f", "parents": ["25313397a4ac3dc5b5c986017bee2a614399509d"], "title": "refactor(schema): command logic removed from commitizen base", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "25313397a4ac3dc5b5c986017bee2a614399509d", "parents": ["d2f13ac41b4e48995b3b619d931c82451886e6ff"], "title": "refactor(info): command logic removed from commitizen base", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "d2f13ac41b4e48995b3b619d931c82451886e6ff", "parents": ["d839e317e5b26671b010584ad8cc6bf362400fa1"], "title": "refactor(example): command logic removed from commitizen base", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "d839e317e5b26671b010584ad8cc6bf362400fa1", "parents": ["12d0e65beda969f7983c444ceedc2a01584f4e08"], "title": "refactor(commit): moved most of the commit logic to the commit command", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "12d0e65beda969f7983c444ceedc2a01584f4e08", "parents": ["fb4c85abe51c228e50773e424cbd885a8b6c610d"], "title": "docs(README): updated documentation url)", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "fb4c85abe51c228e50773e424cbd885a8b6c610d", "parents": ["17efb44d2cd16f6621413691a543e467c7d2dda6"], "title": "docs: mkdocs documentation", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "17efb44d2cd16f6621413691a543e467c7d2dda6", "parents": ["6012d9eecfce8163d75c8fff179788e9ad5347da"], "title": "Bump version 1.0.0 → 1.1.0", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "6012d9eecfce8163d75c8fff179788e9ad5347da", "parents": ["0c7fb0ca0168864dfc55d83c210da57771a18319"], "title": "test: fixed issues with conf", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "0c7fb0ca0168864dfc55d83c210da57771a18319", "parents": ["cb1dd2019d522644da5bdc2594dd6dee17122d7f"], "title": "docs(README): some new information about bump", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "cb1dd2019d522644da5bdc2594dd6dee17122d7f", "parents": ["9c7450f85df6bf6be508e79abf00855a30c3c73c"], "title": "feat: new working bump command", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "9c7450f85df6bf6be508e79abf00855a30c3c73c", "parents": ["9f3af3772baab167e3fd8775d37f041440184251"], "title": "feat: create version tag", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "9f3af3772baab167e3fd8775d37f041440184251", "parents": ["b0d6a3defbfde14e676e7eb34946409297d0221b"], "title": "docs: added new changelog", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "b0d6a3defbfde14e676e7eb34946409297d0221b", "parents": ["d630d07d912e420f0880551f3ac94e933f9d3beb"], "title": "feat: update given files with new version", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "d630d07d912e420f0880551f3ac94e933f9d3beb", "parents": ["1792b8980c58787906dbe6836f93f31971b1ec2d"], "title": "fix: removed all from commit", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "1792b8980c58787906dbe6836f93f31971b1ec2d", "parents": ["52def1ea3555185ba4b936b463311949907e31ec"], "title": "feat(config): new set key, used to set version to cfg", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "52def1ea3555185ba4b936b463311949907e31ec", "parents": ["3127e05077288a5e2b62893345590bf1096141b7"], "title": "feat: support for pyproject.toml", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "3127e05077288a5e2b62893345590bf1096141b7", "parents": ["fd480ed90a80a6ffa540549408403d5b60d0e90c"], "title": "feat: first semantic version bump implementation", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "fd480ed90a80a6ffa540549408403d5b60d0e90c", "parents": ["e4840a059731c0bf488381ffc77e989e85dd81ad"], "title": "fix: fix config file not working", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "e4840a059731c0bf488381ffc77e989e85dd81ad", "parents": ["aa44a92d68014d0da98965c0c2cb8c07957d4362"], "title": "refactor: added commands folder, better integration with decli", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "aa44a92d68014d0da98965c0c2cb8c07957d4362", "parents": ["58bb709765380dbd46b74ce6e8978515764eb955"], "title": "Bump version: 1.0.0b2 → 1.0.0", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "58bb709765380dbd46b74ce6e8978515764eb955", "parents": ["97afb0bb48e72b6feca793091a8a23c706693257"], "title": "docs(README): new badges", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "97afb0bb48e72b6feca793091a8a23c706693257", "parents": [ "9cecb9224aa7fa68d4afeac37eba2a25770ef251", "e004a90b81ea5b374f118759bce5951202d03d69", ], "title": "Merge pull request #10 from Woile/feat/decli", "body": "Feat/decli", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "9cecb9224aa7fa68d4afeac37eba2a25770ef251", "parents": ["f5781d1a2954d71c14ade2a6a1a95b91310b2577"], "title": "style: black to files", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "f5781d1a2954d71c14ade2a6a1a95b91310b2577", "parents": ["80105fb3c6d45369bc0cbf787bd329fba603864c"], "title": "ci: added travis", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "80105fb3c6d45369bc0cbf787bd329fba603864c", "parents": ["a96008496ffefb6b1dd9b251cb479eac6a0487f7"], "title": "refactor: removed delegator, added decli and many tests", "body": "BREAKING CHANGE: API is stable", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "a96008496ffefb6b1dd9b251cb479eac6a0487f7", "parents": ["aab33d13110f26604fb786878856ec0b9e5fc32b"], "title": "docs: updated test command", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "aab33d13110f26604fb786878856ec0b9e5fc32b", "parents": ["b73791563d2f218806786090fb49ef70faa51a3a"], "title": "Bump version: 1.0.0b1 → 1.0.0b2", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "b73791563d2f218806786090fb49ef70faa51a3a", "parents": ["7aa06a454fb717408b3657faa590731fb4ab3719"], "title": "docs(README): updated to reflect current state", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "7aa06a454fb717408b3657faa590731fb4ab3719", "parents": [ "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", "9589a65880016996cff156b920472b9d28d771ca", ], "title": "Merge pull request #9 from Woile/dev", "body": "feat: py3 only, tests and conventional commits 1.0", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", "parents": ["ed830019581c83ba633bfd734720e6758eca6061"], "title": "Bump version: 0.9.11 → 1.0.0b1", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "ed830019581c83ba633bfd734720e6758eca6061", "parents": ["c52eca6f74f844ab3ffbde61d98ef96071e132b7"], "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)", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "c52eca6f74f844ab3ffbde61d98ef96071e132b7", "parents": ["0326652b2657083929507ee66d4d1a0899e861ba"], "title": "Bump version: 0.9.10 → 0.9.11", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "0326652b2657083929507ee66d4d1a0899e861ba", "parents": ["b3f89892222340150e32631ae6b7aab65230036f"], "title": "fix(config): load config reads in order without failing if there is no commitizen section", "body": "Closes #8", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "b3f89892222340150e32631ae6b7aab65230036f", "parents": ["5e837bf8ef0735193597372cd2d85e31a8f715b9"], "title": "Bump version: 0.9.9 → 0.9.10", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "5e837bf8ef0735193597372cd2d85e31a8f715b9", "parents": ["684e0259cc95c7c5e94854608cd3dcebbd53219e"], "title": "fix: parse scope (this is my punishment for not having tests)", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "684e0259cc95c7c5e94854608cd3dcebbd53219e", "parents": ["ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15"], "title": "Bump version: 0.9.8 → 0.9.9", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15", "parents": ["64168f18d4628718c49689ee16430549e96c5d4b"], "title": "fix: parse scope empty", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "64168f18d4628718c49689ee16430549e96c5d4b", "parents": ["9d4def716ef235a1fa5ae61614366423fbc8256f"], "title": "Bump version: 0.9.7 → 0.9.8", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "9d4def716ef235a1fa5ae61614366423fbc8256f", "parents": ["33b0bf1a0a4dc60aac45ed47476d2e5473add09e"], "title": "fix(scope): parse correctly again", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "33b0bf1a0a4dc60aac45ed47476d2e5473add09e", "parents": ["696885e891ec35775daeb5fec3ba2ab92c2629e1"], "title": "Bump version: 0.9.6 → 0.9.7", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "696885e891ec35775daeb5fec3ba2ab92c2629e1", "parents": ["bef4a86761a3bda309c962bae5d22ce9b57119e4"], "title": "fix(scope): parse correctly", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "bef4a86761a3bda309c962bae5d22ce9b57119e4", "parents": ["72472efb80f08ee3fd844660afa012c8cb256e4b"], "title": "Bump version: 0.9.5 → 0.9.6", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "72472efb80f08ee3fd844660afa012c8cb256e4b", "parents": ["b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b"], "title": "refactor(conventionalCommit): moved filters to questions instead of message", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b", "parents": ["3e31714dc737029d96898f412e4ecd2be1bcd0ce"], "title": "fix(manifest): included missing files", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "3e31714dc737029d96898f412e4ecd2be1bcd0ce", "parents": ["9df721e06595fdd216884c36a28770438b4f4a39"], "title": "Bump version: 0.9.4 → 0.9.5", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "9df721e06595fdd216884c36a28770438b4f4a39", "parents": ["0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f"], "title": "fix(config): home path for python versions between 3.0 and 3.5", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f", "parents": ["973c6b3e100f6f69a3fe48bd8ee55c135b96c318"], "title": "Bump version: 0.9.3 → 0.9.4", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "973c6b3e100f6f69a3fe48bd8ee55c135b96c318", "parents": ["dacc86159b260ee98eb5f57941c99ba731a01399"], "title": "feat(cli): added version", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "dacc86159b260ee98eb5f57941c99ba731a01399", "parents": ["4368f3c3cbfd4a1ced339212230d854bc5bab496"], "title": "Bump version: 0.9.2 → 0.9.3", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "4368f3c3cbfd4a1ced339212230d854bc5bab496", "parents": ["da94133288727d35dae9b91866a25045038f2d38"], "title": "feat(committer): conventional commit is a bit more intelligent now", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "da94133288727d35dae9b91866a25045038f2d38", "parents": ["1541f54503d2e1cf39bd777c0ca5ab5eb78772ba"], "title": "docs(README): motivation", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", "parents": ["ddc855a637b7879108308b8dbd85a0fd27c7e0e7"], "title": "Bump version: 0.9.1 → 0.9.2", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "ddc855a637b7879108308b8dbd85a0fd27c7e0e7", "parents": ["46e9032e18a819e466618c7a014bcb0e9981af9e"], "title": "refactor: renamed conventional_changelog to conventional_commits, not backward compatible", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "46e9032e18a819e466618c7a014bcb0e9981af9e", "parents": ["0fef73cd7dc77a25b82e197e7c1d3144a58c1350"], "title": "Bump version: 0.9.0 → 0.9.1", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, { "rev": "0fef73cd7dc77a25b82e197e7c1d3144a58c1350", "parents": [], "title": "fix(setup.py): future is now required for every python version", "body": "", "author": "Commitizen", "author_email": "author@cz.dev", }, ] 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"), ] @pytest.fixture def gitcommits() -> list: commits = [ git.GitCommit( commit["rev"], commit["title"], commit["body"], commit["author"], commit["author_email"], commit["parents"], ) for commit in COMMITS_DATA ] return commits @pytest.fixture def tags() -> list: tags = [git.GitTag(*tag) for tag in TAGS] return tags @pytest.fixture def changelog_content() -> str: changelog_path = "tests/CHANGELOG_FOR_TEST.md" with open(changelog_path, encoding="utf-8") as f: return f.read() 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 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 @pytest.mark.parametrize("test_input", TAGS) def test_valid_tag_included_in_changelog(test_input): tag = git.GitTag(*test_input) rules = changelog.TagRules() assert rules.include_in_changelog(tag) def test_invalid_tag_included_in_changelog(): tag = git.GitTag("not_a_version", "rev", "date") rules = changelog.TagRules() assert not rules.include_in_changelog(tag) COMMITS_TREE = ( { "version": "v1.2.0", "date": "2019-04-19", "changes": { "feat": [ { "scope": None, "breaking": None, "message": "custom cz plugins now support bumping version", } ] }, }, { "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": "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 implementation", }, ], "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)", } ] }, }, { "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", } ] }, }, { "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 filters to questions instead of message", } ], "fix": [ { "scope": "manifest", "breaking": None, "message": "included 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": "committer", "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", } ] }, }, ) COMMITS_TREE_AFTER_MERGED_PRERELEASES = ( { "version": "v1.2.0", "date": "2019-04-19", "changes": { "feat": [ { "scope": None, "breaking": None, "message": "custom cz plugins now support bumping version", } ] }, }, { "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": "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 implementation", }, ], "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", } ], "feat": [ { "scope": None, "breaking": None, "message": "py3 only, tests and conventional commits 1.0", } ], "BREAKING CHANGE": [ {"scope": None, "breaking": None, "message": "API is stable"} ], }, }, { "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)", } ] }, }, { "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", } ] }, }, { "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 filters to questions instead of message", } ], "fix": [ { "scope": "manifest", "breaking": None, "message": "included 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": "committer", "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", } ] }, }, ) @pytest.mark.parametrize("merge_prereleases", (True, False)) def test_generate_tree_from_commits(gitcommits, tags, merge_prereleases): parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.bump_pattern rules = changelog.TagRules( merge_prereleases=merge_prereleases, ) tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern, rules=rules ) expected = ( COMMITS_TREE_AFTER_MERGED_PRERELEASES if merge_prereleases else COMMITS_TREE ) for release, expected_release in zip(tree, expected): assert release["version"] == expected_release["version"] assert release["date"] == expected_release["date"] assert release["changes"].keys() == expected_release["changes"].keys() for change_type in release["changes"]: changes = release["changes"][change_type] expected_changes = expected_release["changes"][change_type] for change, expected_change in zip(changes, expected_changes): assert change["scope"] == expected_change["scope"] assert change["breaking"] == expected_change["breaking"] assert change["message"] == expected_change["message"] assert change["author"] == "Commitizen" assert change["author_email"] in "author@cz.dev" assert "sha1" in change assert "parents" in change def test_generate_tree_from_commits_with_no_commits(tags): gitcommits = [] parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.bump_pattern tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern ) assert tuple(tree) == ({"changes": {}, "date": "", "version": "Unreleased"},) @pytest.mark.parametrize( "change_type_order, expected_reordering", ( ([], {}), ( ["BREAKING CHANGE", "refactor"], { "1.1.0": { "original": ["feat", "fix", "refactor"], "sorted": ["refactor", "feat", "fix"], }, "1.0.0": { "original": ["refactor", "BREAKING CHANGE"], "sorted": ["BREAKING CHANGE", "refactor"], }, }, ), ), ) def test_order_changelog_tree(change_type_order, expected_reordering): tree = changelog.order_changelog_tree(COMMITS_TREE, change_type_order) for index, entry in enumerate(tuple(tree)): version = tree[index]["version"] if version in expected_reordering: # Verify that all keys are present assert [*tree[index].keys()] == [*COMMITS_TREE[index].keys()] # Verify that the reorder only impacted the returned dict and not the original expected = expected_reordering[version] assert [*tree[index]["changes"].keys()] == expected["sorted"] assert [*COMMITS_TREE[index]["changes"].keys()] == expected["original"] else: assert [*entry["changes"].keys()] == [*tree[index]["changes"].keys()] def test_order_changelog_tree_raises(): change_type_order = ["BREAKING CHANGE", "feat", "refactor", "feat"] with pytest.raises(InvalidConfigurationError) as excinfo: changelog.order_changelog_tree(COMMITS_TREE, change_type_order) assert "Change types contain duplicates types" in str(excinfo) def test_render_changelog( gitcommits, tags, changelog_content, any_changelog_format: ChangelogFormat ): parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, template) assert result == changelog_content def test_render_changelog_from_default_plugin_values( gitcommits, tags, changelog_content, any_changelog_format: ChangelogFormat ): parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, template) assert result == changelog_content def test_render_changelog_override_loader(gitcommits, tags, tmp_path: Path): loader = FileSystemLoader(tmp_path) template = "tpl.j2" tpl = "loader overridden" (tmp_path / template).write_text(tpl) parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, template) assert result == tpl def test_render_changelog_override_template_from_cwd( gitcommits, tags, chdir: Path, any_changelog_format: ChangelogFormat ): tpl = "overridden from cwd" template = any_changelog_format.template (chdir / template).write_text(tpl) parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, template) assert result == tpl def test_render_changelog_override_template_from_cwd_with_custom_name( gitcommits, tags, chdir: Path ): tpl = "template overridden from cwd" tpl_name = "tpl.j2" (chdir / tpl_name).write_text(tpl) parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, tpl_name) assert result == tpl def test_render_changelog_override_loader_and_template( gitcommits, tags, tmp_path: Path ): loader = FileSystemLoader(tmp_path) tpl = "loader and template overridden" tpl_name = "tpl.j2" (tmp_path / tpl_name).write_text(tpl) parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.bump_pattern tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, tpl_name) assert result == tpl def test_render_changelog_support_arbitrary_kwargs(gitcommits, tags, tmp_path: Path): loader = FileSystemLoader(tmp_path) tpl_name = "tpl.j2" (tmp_path / tpl_name).write_text("{{ key }}") parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, tpl_name, key="value") assert result == "value" def test_render_changelog_unreleased(gitcommits, any_changelog_format: ChangelogFormat): some_commits = gitcommits[:7] parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( some_commits, [], parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, template) assert "Unreleased" in result def test_render_changelog_tag_and_unreleased( gitcommits, tags, any_changelog_format: ChangelogFormat ): some_commits = gitcommits[:7] single_tag = [ tag for tag in tags if tag.rev == "56c8a8da84e42b526bcbe130bd194306f7c7e813" ] parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( some_commits, single_tag, parser, changelog_pattern ) result = changelog.render_changelog(tree, loader, template) assert "Unreleased" in result assert "## v1.1.1" in result def test_render_changelog_with_change_type( gitcommits, tags, any_changelog_format: ChangelogFormat ): new_title = ":some-emoji: feature" change_type_map = {"feat": new_title} parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern, change_type_map=change_type_map ) result = changelog.render_changelog(tree, loader, template) assert new_title in result def test_render_changelog_with_changelog_message_builder_hook( gitcommits, tags, any_changelog_format: ChangelogFormat ): def changelog_message_builder_hook(message: dict, commit: git.GitCommit) -> dict: message["message"] = ( f"{message['message']} [link](github.com/232323232) {commit.author} {commit.author_email}" ) return message parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern, changelog_message_builder_hook=changelog_message_builder_hook, ) result = changelog.render_changelog(tree, loader, template) assert "[link](github.com/232323232) Commitizen author@cz.dev" in result def test_changelog_message_builder_hook_can_remove_commits( gitcommits, tags, any_changelog_format: ChangelogFormat ): def changelog_message_builder_hook(message: dict, commit: git.GitCommit): return None parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern, changelog_message_builder_hook=changelog_message_builder_hook, ) result = changelog.render_changelog(tree, loader, template) RE_HEADER = re.compile(r"^## v?\d+\.\d+\.\d+(\w)* \(\d{4}-\d{2}-\d{2}\)$") # Rendered changelog should be empty, only containing version headers for no, line in enumerate(result.splitlines()): if line := line.strip(): assert RE_HEADER.match(line), f"Line {no} should not be there: {line}" def test_render_changelog_with_changelog_message_builder_hook_multiple_entries( gitcommits, tags, any_changelog_format: ChangelogFormat ): def changelog_message_builder_hook(message: dict, commit: git.GitCommit): messages = [message.copy(), message.copy(), message.copy()] for idx, msg in enumerate(messages): msg["message"] = "Message #{idx}" return messages parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern, changelog_message_builder_hook=changelog_message_builder_hook, ) result = changelog.render_changelog(tree, loader, template) for idx in range(3): assert "Message #{idx}" in result def test_changelog_message_builder_hook_can_access_and_modify_change_type( gitcommits, tags, any_changelog_format: ChangelogFormat ): def changelog_message_builder_hook(message: dict, commit: git.GitCommit): assert "change_type" in message message["change_type"] = "overridden" return message parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern loader = ConventionalCommitsCz.template_loader template = any_changelog_format.template tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern, changelog_message_builder_hook=changelog_message_builder_hook, ) result = changelog.render_changelog(tree, loader, template) RE_HEADER = re.compile(r"^### (?P.+)$") # There should be only "overridden" change type headers for no, line in enumerate(result.splitlines()): if (line := line.strip()) and (match := RE_HEADER.match(line)): change_type = match.group("type") assert change_type == "overridden", ( f"Line {no}: type {change_type} should have been overridden" ) def test_render_changelog_with_changelog_release_hook( gitcommits, tags, any_changelog_format: ChangelogFormat ): def changelog_release_hook(release: dict, tag: Optional[git.GitTag]) -> dict: release["extra"] = "whatever" return release parser = ConventionalCommitsCz.commit_parser changelog_pattern = ConventionalCommitsCz.changelog_pattern tree = changelog.generate_tree_from_commits( gitcommits, tags, parser, changelog_pattern, changelog_release_hook=changelog_release_hook, ) for release in tree: assert release["extra"] == "whatever" def test_get_smart_tag_range_returns_an_extra_for_a_range(tags): start, end = ( tags[0], tags[2], ) # len here is 3, but we expect one more tag as designed res = changelog.get_smart_tag_range(tags, start.name, end.name) assert 4 == len(res) def test_get_smart_tag_range_returns_an_extra_for_a_single_tag(tags): start = tags[0] # len here is 1, but we expect one more tag as designed res = changelog.get_smart_tag_range(tags, start.name) assert 2 == len(res) @dataclass class TagDef: name: str is_version: bool is_legacy: bool is_ignored: bool TAGS_PARAMS = ( pytest.param(TagDef("1.2.3", True, False, False), id="version"), # We test with `v-` prefix as `v` prefix is a special case kept for backward compatibility pytest.param(TagDef("v-1.2.3", False, True, False), id="v-prefix"), pytest.param(TagDef("project-1.2.3", False, True, False), id="project-prefix"), pytest.param(TagDef("ignored", False, False, True), id="ignored"), pytest.param(TagDef("unknown", False, False, False), id="unknown"), ) @pytest.mark.parametrize("tag", TAGS_PARAMS) def test_tag_rules_tag_format_only(tag: TagDef): rules = changelog.TagRules(Pep440, "$version") assert rules.is_version_tag(tag.name) is tag.is_version @pytest.mark.parametrize("tag", TAGS_PARAMS) def test_tag_rules_with_legacy_tags(tag: TagDef): rules = changelog.TagRules( scheme=Pep440, tag_format="$version", legacy_tag_formats=["v-$version", "project-${version}"], ) assert rules.is_version_tag(tag.name) is tag.is_version or tag.is_legacy @pytest.mark.parametrize("tag", TAGS_PARAMS) def test_tag_rules_with_ignored_tags(tag: TagDef): rules = changelog.TagRules( scheme=Pep440, tag_format="$version", ignored_tag_formats=["ignored"] ) assert rules.is_ignored_tag(tag.name) is tag.is_ignored def test_tags_rules_get_version_tags(capsys: pytest.CaptureFixture): tags = [ git.GitTag("v1.1.0", "17efb44d2cd16f6621413691a543e467c7d2dda6", "2019-04-14"), git.GitTag("v1.0.0", "aa44a92d68014d0da98965c0c2cb8c07957d4362", "2019-03-01"), git.GitTag("1.0.0b2", "aab33d13110f26604fb786878856ec0b9e5fc32b", "2019-01-18"), git.GitTag( "project-not-a-version", "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", "2019-01-17", ), git.GitTag( "not-a-version", "c52eca6f74f844ab3ffbde61d98ef96071e132b7", "2018-12-17" ), git.GitTag( "star-something", "c52eca6f74f844ab3ffbde61d98fe96071e132b2", "2018-11-12" ), git.GitTag("known", "b3f89892222340150e32631ae6b7aab65230036f", "2018-09-22"), git.GitTag( "ignored-0.9.3", "684e0259cc95c7c5e94854608cd3dcebbd53219e", "2018-09-22" ), git.GitTag( "project-0.9.3", "dacc86159b260ee98eb5f57941c99ba731a01399", "2018-07-28" ), git.GitTag( "anything-0.9", "5141f54503d2e1cf39bd666c0ca5ab5eb78772ab", "2018-01-10" ), git.GitTag( "project-0.9.2", "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", "2017-11-11" ), git.GitTag( "ignored-0.9.1", "46e9032e18a819e466618c7a014bcb0e9981af9e", "2017-11-11" ), ] rules = changelog.TagRules( scheme=Pep440, tag_format="v$version", legacy_tag_formats=["$version", "project-${version}"], ignored_tag_formats=[ "known", "ignored-${version}", "star-*", "*-${major}.${minor}", ], ) version_tags = rules.get_version_tags(tags, warn=True) assert {t.name for t in version_tags} == { "v1.1.0", "v1.0.0", "1.0.0b2", "project-0.9.3", "project-0.9.2", } captured = capsys.readouterr() assert captured.err.count("InvalidVersion") == 2 assert captured.err.count("not-a-version") == 2