diff --git a/commitizen/bump.py b/commitizen/bump.py index 6c09e2f16d..2b5d77a9f2 100644 --- a/commitizen/bump.py +++ b/commitizen/bump.py @@ -21,7 +21,7 @@ def find_increment( # Most important cases are major and minor. # Everything else will be considered patch. select_pattern = re.compile(regex) - increment = None + increment: Optional[str] = None for commit in commits: for message in commit.message.split("\n"): @@ -196,7 +196,9 @@ def _version_to_regex(version: str): return re.compile(f"{clean_regex}") -def create_tag(version: Union[Version, str], tag_format: Optional[str] = None) -> str: +def normalize_tag( + version: Union[Version, str], tag_format: Optional[str] = None +) -> str: """The tag and the software version might be different. That's why this function exists. diff --git a/commitizen/changelog.py b/commitizen/changelog.py index d854219a28..0738c2e281 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -29,11 +29,12 @@ import re from collections import OrderedDict, defaultdict from datetime import date -from typing import Callable, Dict, Iterable, List, Optional +from typing import Callable, Dict, Iterable, List, Optional, Tuple from jinja2 import Environment, PackageLoader from commitizen import defaults +from commitizen.bump import normalize_tag from commitizen.exceptions import InvalidConfigurationError from commitizen.git import GitCommit, GitTag @@ -281,3 +282,71 @@ def incremental_build(new_content: str, lines: List, metadata: Dict) -> List: if not isinstance(latest_version_position, int): output_lines.append(new_content) return output_lines + + +def get_smart_tag_range( + tags: List[GitTag], newest: str, oldest: Optional[str] = None +) -> List[GitTag]: + """Smart because it finds the N+1 tag. + + This is because we need to find until the next tag + """ + accumulator = [] + keep = False + if not oldest: + oldest = newest + for index, tag in enumerate(tags): + if tag.name == newest: + keep = True + if keep: + accumulator.append(tag) + if tag.name == oldest: + keep = False + try: + accumulator.append(tags[index + 1]) + except IndexError: + pass + break + return accumulator + + +def get_oldest_and_newest_rev( + tags: List[GitTag], version: str, tag_format: str +) -> Tuple[Optional[str], Optional[str]]: + """Find the tags for the given version. + + `version` may come in different formats: + - `0.1.0..0.4.0`: as a range + - `0.3.0`: as a single version + """ + oldest: Optional[str] = None + newest: Optional[str] = None + try: + oldest, newest = version.split("..") + except ValueError: + newest = version + + newest_tag = normalize_tag(newest, tag_format=tag_format) + + oldest_tag = None + if oldest: + oldest_tag = normalize_tag(oldest, tag_format=tag_format) + + tags_range = get_smart_tag_range(tags, newest=newest_tag, oldest=oldest_tag) + if not tags_range: + return None, None + + oldest_rev: Optional[str] = tags_range[-1].name + newest_rev = newest_tag + + # check if it's the first tag created + # and it's also being requested as part of the range + if oldest_rev == tags[-1].name and oldest_rev == oldest_tag: + return None, newest_rev + + # when they are the same, and it's also the + # first tag created + if oldest_rev == newest_rev: + return None, newest_rev + + return oldest_rev, newest_rev diff --git a/commitizen/cli.py b/commitizen/cli.py index d582a27a18..839e6cf5d7 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -188,6 +188,12 @@ "useful if the changelog has been manually modified" ), }, + { + "name": "rev_range", + "type": str, + "nargs": "?", + "help": "generates changelog for the given version (e.g: 1.5.3) or version range (e.g: 1.5.3..1.7.9)", + }, { "name": "--start-rev", "default": None, diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 70dcad263f..cd05e8a8f3 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -101,7 +101,7 @@ def __call__(self): # noqa: C901 is_files_only: Optional[bool] = self.arguments["files_only"] is_local_version: Optional[bool] = self.arguments["local_version"] - current_tag_version: str = bump.create_tag( + current_tag_version: str = bump.normalize_tag( current_version, tag_format=tag_format ) @@ -149,7 +149,7 @@ def __call__(self): # noqa: C901 is_local_version=is_local_version, ) - new_tag_version = bump.create_tag(new_version, tag_format=tag_format) + new_tag_version = bump.normalize_tag(new_version, tag_format=tag_format) message = bump.create_commit_message( current_version, new_version, bump_commit_message ) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 535632dfc0..58c0ceaeef 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -11,6 +11,7 @@ NoPatternMapError, NoRevisionError, NotAGitProjectError, + NotAllowed, ) from commitizen.git import GitTag @@ -46,6 +47,10 @@ def __init__(self, config: BaseConfig, args): self.change_type_order = ( self.config.settings.get("change_type_order") or self.cz.change_type_order ) + self.rev_range = args.get("rev_range") + self.tag_format = args.get("tag_format") or self.config.settings.get( + "tag_format" + ) def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str: """Try to find the 'start_rev'. @@ -73,6 +78,30 @@ def _find_incremental_rev(self, latest_version: str, tags: List[GitTag]) -> str: start_rev = tag.name return start_rev + def write_changelog( + self, changelog_out: str, lines: List[str], changelog_meta: Dict + ): + if not isinstance(self.file_name, str): + raise NotAllowed( + "Changelog file name is broken.\n" + "Check the flag `--file-name` in the terminal " + f"or the setting `changelog_file` in {self.config.path}" + ) + + changelog_hook: Optional[Callable] = self.cz.changelog_hook + with open(self.file_name, "w") as changelog_file: + partial_changelog: Optional[str] = None + if self.incremental: + new_lines = changelog.incremental_build( + changelog_out, lines, changelog_meta + ) + changelog_out = "".join(new_lines) + partial_changelog = changelog_out + + if changelog_hook: + changelog_out = changelog_hook(changelog_out, partial_changelog) + changelog_file.write(changelog_out) + def __call__(self): commit_parser = self.cz.commit_parser changelog_pattern = self.cz.changelog_pattern @@ -83,23 +112,37 @@ def __call__(self): changelog_message_builder_hook: Optional[ Callable ] = self.cz.changelog_message_builder_hook - changelog_hook: Optional[Callable] = self.cz.changelog_hook + if not changelog_pattern or not commit_parser: raise NoPatternMapError( f"'{self.config.settings['name']}' rule does not support changelog" ) + if self.incremental and self.rev_range: + raise NotAllowed("--incremental cannot be combined with a rev_range") + tags = git.get_tags() if not tags: tags = [] + end_rev = "" + if self.incremental: changelog_meta = changelog.get_metadata(self.file_name) latest_version = changelog_meta.get("latest_version") if latest_version: start_rev = self._find_incremental_rev(latest_version, tags) - commits = git.get_commits(start=start_rev, args="--author-date-order") + if self.rev_range and self.tag_format: + start_rev, end_rev = changelog.get_oldest_and_newest_rev( + tags, + version=self.rev_range, + tag_format=self.tag_format, + ) + + commits = git.get_commits( + start=start_rev, end=end_rev, args="--author-date-order" + ) if not commits: raise NoCommitsFoundError("No commits found") @@ -126,15 +169,4 @@ def __call__(self): with open(self.file_name, "r") as changelog_file: lines = changelog_file.readlines() - with open(self.file_name, "w") as changelog_file: - partial_changelog: Optional[str] = None - if self.incremental: - new_lines = changelog.incremental_build( - changelog_out, lines, changelog_meta - ) - changelog_out = "".join(new_lines) - partial_changelog = changelog_out - - if changelog_hook: - changelog_out = changelog_hook(changelog_out, partial_changelog) - changelog_file.write(changelog_out) + self.write_changelog(changelog_out, lines, changelog_meta) diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py index c2a90cec9f..0d09c90796 100644 --- a/commitizen/config/toml_config.py +++ b/commitizen/config/toml_config.py @@ -49,7 +49,7 @@ def _parse_setting(self, data: Union[bytes, str]) -> None: name = "cz_conventional_commits" ``` """ - doc = parse(data) # type: ignore + doc = parse(data) try: self.settings.update(doc["tool"]["commitizen"]) # type: ignore except exceptions.NonExistentKey: diff --git a/commitizen/cz/__init__.py b/commitizen/cz/__init__.py index 05e54673fb..f141e1c256 100644 --- a/commitizen/cz/__init__.py +++ b/commitizen/cz/__init__.py @@ -23,7 +23,7 @@ def discover_plugins(path: Iterable[str] = None) -> Dict[str, Type[BaseCommitize for finder, name, ispkg in pkgutil.iter_modules(path): try: if name.startswith("cz_"): - plugins[name] = importlib.import_module(name).discover_this # type: ignore + plugins[name] = importlib.import_module(name).discover_this except AttributeError as e: warnings.warn(UserWarning(e.args[0])) continue diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index 218534c730..9ae97ee1b0 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -24,6 +24,7 @@ class ExitCode(enum.IntEnum): CURRENT_VERSION_NOT_FOUND = 17 INVALID_COMMAND_ARGUMENT = 18 INVALID_CONFIGURATION = 19 + NOT_ALLOWED = 20 class CommitizenException(Exception): @@ -142,3 +143,7 @@ class InvalidCommandArgumentError(CommitizenException): class InvalidConfigurationError(CommitizenException): exit_code = ExitCode.INVALID_CONFIGURATION + + +class NotAllowed(CommitizenException): + exit_code = ExitCode.NOT_ALLOWED diff --git a/commitizen/git.py b/commitizen/git.py index 4e60b68793..98ee40f2f2 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -39,11 +39,15 @@ class GitTag(GitObject): def __init__(self, name, rev, date): self.rev = rev.strip() self.name = name.strip() - self.date = date.strip() + self._date = date.strip() def __repr__(self): return f"GitTag('{self.name}', '{self.rev}', '{self.date}')" + @property + def date(self): + return self._date + @classmethod def from_line(cls, line: str, inner_delimiter: str) -> "GitTag": @@ -82,10 +86,10 @@ def get_commits( ) if start: - c = cmd.run(f"{git_log_cmd} {start}..{end}") + command = f"{git_log_cmd} {start}..{end}" else: - c = cmd.run(f"{git_log_cmd} {end}") - + command = f"{git_log_cmd} {end}" + c = cmd.run(command) if not c.out: return [] diff --git a/docs/changelog.md b/docs/changelog.md index f5dd5fd5e3..6f92bb21cd 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,9 +6,11 @@ This command will generate a changelog following the committing rules establishe ```bash $ cz changelog --help -usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] - [--unreleased-version UNRELEASED_VERSION] [--incremental] - [--start-rev START_REV] +usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] [--unreleased-version UNRELEASED_VERSION] [--incremental] [--start-rev START_REV] + [rev_range] + +positional arguments: + rev_range generates changelog for the given version (e.g: 1.5.3) or version range (e.g: 1.5.3..1.7.9) optional arguments: -h, --help show this help message and exit @@ -16,17 +18,16 @@ optional arguments: --file-name FILE_NAME file name of changelog (default: 'CHANGELOG.md') --unreleased-version UNRELEASED_VERSION - set the value for the new version (use the tag value), - instead of using unreleased - --incremental generates changelog from last created version, useful - if the changelog has been manually modified + set the value for the new version (use the tag value), instead of using unreleased + --incremental generates changelog from last created version, useful if the changelog has been manually modified --start-rev START_REV - start rev of the changelog.If not set, it will - generate changelog from the start + start rev of the changelog.If not set, it will generate changelog from the start ``` ### Examples +#### Generate full changelog + ```bash cz changelog ``` @@ -35,6 +36,18 @@ cz changelog cz ch ``` +#### Get the changelog for the given version + +```bash +cz changelog 0.3.0 +``` + +#### Get the changelog for the given version range + +```bash +cz changelog 0.3.0..0.4.0 +``` + ## Constrains changelog generation is constrained only to **markdown** files. @@ -66,7 +79,7 @@ and the following variables are expected: | `change_type` | The group where the commit belongs to, this is optional. Example: fix | `commit regex` | | `message`\* | Information extracted from the commit message | `commit regex` | | `scope` | Contextual information. Should be parsed using the regex from the message, it will be **bold** | `commit regex` | -| `breaking` | Whether is a breaking change or not | `commit regex` | +| `breaking` | Whether is a breaking change or not | `commit regex` | - **required**: is the only one required to be parsed by the regex @@ -74,7 +87,7 @@ and the following variables are expected: ### `unreleased_version` -There is usually an egg and chicken situation when automatically +There is usually a chicken and egg situation when automatically bumping the version and creating the changelog. If you bump the version first, you have no changelog, you have to create it later, and it won't be included in diff --git a/docs/config.md b/docs/config.md index cc278e3865..a7bb5d217d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -122,17 +122,21 @@ commitizen: ## Settings -| Variable | Type | Default | Description | -| ---------------- | ------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | `str` | `"cz_conventional_commits"` | Name of the committing rules to use | -| `version` | `str` | `None` | Current version. Example: "0.1.2" | -| `version_files` | `list` | `[ ]` | Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [See more][version_files] | -| `tag_format` | `str` | `None` | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more][tag_format] | -| `bump_message` | `str` | `None` | Create custom commit message, useful to skip ci. [See more][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)][additional-features] | -| `customize` | `dict` | `None` | **This is only supported when config through `toml`.** Custom rules for committing and bumping. [See more][customization] | -| `use_shortcuts` | `bool` | `false` | If enabled, commitizen will show keyboard shortcuts when selecting from a list. Define a `key` for each of your choices to set the key. [See more][shortcuts] | +| Variable | Type | Default | Description | +| -------------------------- | ------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `str` | `"cz_conventional_commits"` | Name of the committing rules to use | +| `version` | `str` | `None` | Current version. Example: "0.1.2" | +| `version_files` | `list` | `[ ]` | Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [See more][version_files] | +| `tag_format` | `str` | `None` | Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [See more][tag_format] | +| `update_changelog_on_bump` | `bool` | `false` | Create changelog when running `cz bump` | +| `annotated_tag` | `bool` | `false` | Use annotated tags instead of lightweight tags. [See difference][annotated-tags-vs-lightweight] | +| `bump_message` | `str` | `None` | Create custom commit message, useful to skip ci. [See more][bump_message] | +| `changelog_file` | `str` | `CHANGELOG.md` | filename of exported changelog | +| `changelog_incremental` | `bool` | `false` | Update changelog with the missing versions. This is good if you don't want to replace previous versions in the file. Note: when doing `cz bump --changelog` this is automatically set to `true` | +| `changelog_start_rev` | `str` | `None` | Start from a given git rev to generate the 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)][additional-features] | +| `customize` | `dict` | `None` | **This is only supported when config through `toml`.** Custom rules for committing and bumping. [See more][customization] | +| `use_shortcuts` | `bool` | `false` | If enabled, commitizen will show keyboard shortcuts when selecting from a list. Define a `key` for each of your choices to set the key. [See more][shortcuts] | [version_files]: bump.md#version_files [tag_format]: bump.md#tag_format @@ -140,3 +144,4 @@ commitizen: [additional-features]: https://github.com/tmbo/questionary#additional-features [customization]: customization.md [shortcuts]: customization.md#shortcut-keys +[annotated-tags-vs-lightweight]: https://stackoverflow.com/a/11514139/2047185 diff --git a/pyproject.toml b/pyproject.toml index 5f2e05b816..78ac64c68b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,13 +72,14 @@ isort = "^5.7.0" # linter flake8 = "^3.6" pre-commit = "^2.6.0" -mypy = "0.910" +mypy = "^0.931" types-PyYAML = "^5.4.3" types-termcolor = "^0.1.1" # documentation mkdocs = "^1.0" mkdocs-material = "^4.1" pydocstyle = "^5.0.2" +pytest-xdist = "^2.5.0" [tool.poetry.scripts] cz = "commitizen.cli:main" diff --git a/scripts/test b/scripts/test index 08c54035dc..b5541d2d3e 100755 --- a/scripts/test +++ b/scripts/test @@ -5,7 +5,7 @@ if [ -d 'venv' ] ; then export PREFIX="venv/bin/" fi -${PREFIX}pytest --cov-report term-missing --cov-report=xml:coverage.xml --cov=commitizen tests/ +${PREFIX}pytest -n 3 --cov-report term-missing --cov-report=xml:coverage.xml --cov=commitizen tests/ ${PREFIX}black commitizen tests --check ${PREFIX}isort --check-only commitizen tests ${PREFIX}flake8 commitizen/ tests/ diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index 02e0e74644..223c7bf1f1 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -10,8 +10,9 @@ NoCommitsFoundError, NoRevisionError, NotAGitProjectError, + NotAllowed, ) -from tests.utils import create_file_and_commit +from tests.utils import create_file_and_commit, wait_for_tag @pytest.mark.usefixtures("tmp_commitizen_project") @@ -279,7 +280,7 @@ def test_changelog_multiple_incremental_do_not_add_new_lines( mocker.patch.object(sys, "argv", testargs) cli.main() - create_file_and_commit("fix: mama gotta work") + create_file_and_commit("fix: no more explosions") testargs = ["cz", "changelog", "--incremental"] mocker.patch.object(sys, "argv", testargs) @@ -486,7 +487,7 @@ def test_changelog_incremental_keep_a_changelog_sample_with_annotated_tag( @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.freeze_time("2021-06-11") def test_changelog_incremental_with_release_candidate_version( - mocker, capsys, changelog_path, file_regression, test_input + mocker, changelog_path, file_regression, test_input ): """Fix #357""" with open(changelog_path, "w") as f: @@ -514,3 +515,314 @@ def test_changelog_incremental_with_release_candidate_version( out = f.read() file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_with_filename_as_empty_string(mocker, changelog_path, config_path): + + with open(config_path, "a") as f: + f.write("changelog_file = true\n") + + create_file_and_commit("feat: add new output") + + testargs = ["cz", "changelog"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_first_version_from_arg( + mocker, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "changelog", "0.2.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path, "r") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_latest_version_from_arg( + mocker, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, "r") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_single_version_not_found( + mocker, config_path, changelog_path +): + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + testargs = ["cz", "changelog", "0.8.0"] # it shouldn't exist + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(NoCommitsFoundError) as excinfo: + cli.main() + + assert "No commits found" in str(excinfo) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_range_version_not_found(mocker, config_path): + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "changelog", "0.5.0..0.8.0"] # it shouldn't exist + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(NoCommitsFoundError) as excinfo: + cli.main() + + assert "No commits found" in str(excinfo) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_version_range_including_first_tag( + mocker, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "changelog", "0.2.0..0.3.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path, "r") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_version_range_from_arg( + mocker, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: getting ready for this") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0..0.4.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path, "r") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_version_with_big_range_from_arg( + mocker, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] # 0.3.0 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: getting ready for this") + + testargs = ["cz", "bump", "--yes"] # 0.4.0 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("fix: small error") + + testargs = ["cz", "bump", "--yes"] # 0.4.1 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: new shinny feature") + + testargs = ["cz", "bump", "--yes"] # 0.5.0 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: amazing different shinny feature") + # dirty hack to avoid same time between tags + + testargs = ["cz", "bump", "--yes"] # 0.6.0 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0..0.5.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path, "r") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_latest_version_dry_run( + mocker, capsys, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + capsys.readouterr() + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + + out, _ = capsys.readouterr() + + file_regression.check(out, extension=".md") diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_first_version_from_arg.md b/tests/commands/test_changelog_command/test_changelog_from_rev_first_version_from_arg.md new file mode 100644 index 0000000000..3519498ac2 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_first_version_from_arg.md @@ -0,0 +1,5 @@ +## 0.2.0 (2022-02-13) + +### Feat + +- new file diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_dry_run.md b/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_dry_run.md new file mode 100644 index 0000000000..e6531e6b3c --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_dry_run.md @@ -0,0 +1,7 @@ +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 + diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_from_arg.md b/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_from_arg.md new file mode 100644 index 0000000000..91880643eb --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_from_arg.md @@ -0,0 +1,6 @@ +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_from_arg.md b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_from_arg.md new file mode 100644 index 0000000000..0c483c740a --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_from_arg.md @@ -0,0 +1,12 @@ +## 0.4.0 (2022-02-13) + +### Feat + +- getting ready for this + +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_including_first_tag.md b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_including_first_tag.md new file mode 100644 index 0000000000..44bffb319d --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_including_first_tag.md @@ -0,0 +1,12 @@ +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 + +## 0.2.0 (2022-02-13) + +### Feat + +- new file diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_version_with_big_range_from_arg.md b/tests/commands/test_changelog_command/test_changelog_from_rev_version_with_big_range_from_arg.md new file mode 100644 index 0000000000..376424db3e --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_version_with_big_range_from_arg.md @@ -0,0 +1,24 @@ +## 0.5.0 (2022-02-13) + +### Feat + +- new shinny feature + +## 0.4.1 (2022-02-13) + +### Fix + +- small error + +## 0.4.0 (2022-02-13) + +### Feat + +- getting ready for this + +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 diff --git a/tests/test_bump_create_tag.py b/tests/test_bump_normalize_tag.py similarity index 93% rename from tests/test_bump_create_tag.py rename to tests/test_bump_normalize_tag.py index 0d6ee60d69..3bc9828a2f 100644 --- a/tests/test_bump_create_tag.py +++ b/tests/test_bump_normalize_tag.py @@ -19,5 +19,5 @@ @pytest.mark.parametrize("test_input,expected", conversion) def test_create_tag(test_input, expected): version, format = test_input - new_tag = bump.create_tag(Version(version), format) + new_tag = bump.normalize_tag(Version(version), format) assert new_tag == expected diff --git a/tests/test_changelog.py b/tests/test_changelog.py index b3071ed5fc..e68a3abdcf 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -912,3 +912,18 @@ def changelog_message_builder_hook(message: dict, commit: git.GitCommit) -> dict result = changelog.render_changelog(tree) assert "[link](github.com/232323232) Commitizen author@cz.dev" in result + + +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) diff --git a/tests/test_changelog_parser.py b/tests/test_changelog_parser.py index ae6df53912..540f62fd19 100644 --- a/tests/test_changelog_parser.py +++ b/tests/test_changelog_parser.py @@ -33,15 +33,17 @@ def changelog_content() -> str: @pytest.fixture -def existing_changelog_file(): - changelog_path = "tests/CHANGELOG.md" +def existing_changelog_file(tmpdir): + with tmpdir.as_cwd(): + changelog_path = os.path.join(os.getcwd(), "CHANGELOG.md") + # changelog_path = "tests/CHANGELOG.md" - with open(changelog_path, "w") as f: - f.write(CHANGELOG_TEMPLATE) + with open(changelog_path, "w") as f: + f.write(CHANGELOG_TEMPLATE) - yield changelog_path + yield changelog_path - os.remove(changelog_path) + os.remove(changelog_path) def test_read_changelog_blocks(existing_changelog_file): diff --git a/tests/utils.py b/tests/utils.py index 7f5b2b87f3..a1583490b6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,3 +1,4 @@ +import time import uuid from pathlib import Path from typing import Optional @@ -19,3 +20,17 @@ def create_file_and_commit(message: str, filename: Optional[str] = None): Path(f"./{filename}").touch() cmd.run("git add .") git.commit(message) + + +def wait_for_tag(): + """Wait for tag. + + The resolution of timestamps is 1 second, so we need to wait + to create another tag unfortunately. + + This means: + If we create 2 tags under the same second, they might be returned in the wrong order + + See https://stackoverflow.com/questions/28237043/what-is-the-resolution-of-gits-commit-date-or-author-date-timestamps + """ + time.sleep(1.1)