diff --git a/commitizen/cli.py b/commitizen/cli.py index 0436155d05..a5c336f9fd 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -142,6 +142,12 @@ "help": "create annotated tag instead of lightweight one", "action": "store_true", }, + { + "name": ["--changelog-to-stdout"], + "action": "store_true", + "default": False, + "help": "Output changelog to the stdout", + }, ], }, { diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 16c12de0ec..4f2a0d3981 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -46,6 +46,7 @@ def __init__(self, config: BaseConfig, arguments: dict): self.changelog = arguments["changelog"] or self.config.settings.get( "update_changelog_on_bump" ) + self.changelog_to_stdout = arguments["changelog_to_stdout"] self.no_verify = arguments["no_verify"] self.check_consistency = arguments["check_consistency"] @@ -110,6 +111,11 @@ def __call__(self): # noqa: C901 else: commits = git.get_commits(current_tag_version) + # If user specified changelog_to_stdout, they probably want the + # changelog to be generated as well, this is the most intuitive solution + if not self.changelog and self.changelog_to_stdout: + self.changelog = True + # No commits, there is no need to create an empty tag. # Unless we previously had a prerelease. if not commits and not current_version_instance.is_prerelease: @@ -149,12 +155,20 @@ def __call__(self): # noqa: C901 ) # Report found information - out.write( + information = ( f"{message}\n" f"tag to create: {new_tag_version}\n" f"increment detected: {increment}\n" ) + if self.changelog_to_stdout: + # When the changelog goes to stdout, we want to send + # the bump information to stderr, this way the + # changelog output can be captured + out.diagnostic(information) + else: + out.write(information) + if increment is None and new_tag_version == current_tag_version: raise NoneIncrementExit() @@ -170,6 +184,19 @@ def __call__(self): # noqa: C901 ) if self.changelog: + if self.changelog_to_stdout: + changelog_cmd = Changelog( + self.config, + { + "unreleased_version": new_tag_version, + "incremental": True, + "dry_run": True, + }, + ) + try: + changelog_cmd() + except DryRunExit: + pass changelog_cmd = Changelog( self.config, { @@ -196,7 +223,12 @@ def __call__(self): # noqa: C901 ) if c.return_code != 0: raise BumpTagFailedError(c.err) - out.success("Done!") + + # TODO: For v3 output this only as diagnostic and remove this if + if self.changelog_to_stdout: + out.diagnostic("Done!") + else: + out.success("Done!") def _get_commit_args(self): commit_args = ["-a"] diff --git a/commitizen/out.py b/commitizen/out.py index 268f02e29f..7ac5ba420b 100644 --- a/commitizen/out.py +++ b/commitizen/out.py @@ -26,3 +26,7 @@ def success(value: str): def info(value: str): message = colored(value, "blue") line(message) + + +def diagnostic(value: str): + line(value, file=sys.stderr) diff --git a/docs/bump.md b/docs/bump.md index 086cfa2ea2..bf3b207cd6 100644 --- a/docs/bump.md +++ b/docs/bump.md @@ -139,7 +139,6 @@ However, it will still update `pyproject.toml` and `src/__version__.py`. To fix it, you'll first `git checkout .` to reset to the status before trying to bump and update the version in `setup.py` to `1.21.0` - ### `--local-version` Bump the local portion of the version. @@ -161,6 +160,25 @@ If `--local-version` is used, it will bump only the local version `0.1.0` and ke If `--annotated-tag` is used, commitizen will create annotated tags. Also available via configuration, in `pyproject.toml` or `.cz.toml`. +### `--changelog-to-stdout` + +If `--changelog-to-stdout` is used, the incremental changelog generated by the bump +will be sent to the stdout, and any other message generated by the bump will be +sent to stderr. + +If `--changelog` is not used with this command, it is still smart enough to +understand that the user wants to create a changelog. It is recommened to be +explicit and use `--changelog` (or the setting `update_changelog_on_bump`). + +This command is useful to "transport" the newly created changelog. +It can be sent to an auditing system, or to create a Github Release. + +Example: + +```bash +cz bump --changelog --changelog-to-stdout > body.md +``` + ## Configuration ### `tag_format` @@ -198,7 +216,7 @@ Supported variables: --- -### `version_files` * +### `version_files` \* It is used to identify the files which should be updated with the new version. It is also possible to provide a pattern for each file, separated by colons (`:`). diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index ff4acd7a04..8508c66eee 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -414,3 +414,21 @@ def test_prevent_prerelease_when_no_increment_detected( "[NO_COMMITS_FOUND]\n" "No commits found to generate a pre-release." ) assert expected_error_message in str(excinfo.value) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_with_changelog_to_stdout_arg(mocker, capsys, changelog_path): + create_file_and_commit("feat(user): this should appear in stdout") + testargs = ["cz", "bump", "--yes", "--changelog-to-stdout"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, _ = capsys.readouterr() + + assert "this should appear in stdout" in out + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + with open(changelog_path, "r") as f: + out = f.read() + assert out.startswith("#") + assert "0.2.0" in out diff --git a/tests/commands/test_init_command.py b/tests/commands/test_init_command.py index 977e9e6a11..729950fc8a 100644 --- a/tests/commands/test_init_command.py +++ b/tests/commands/test_init_command.py @@ -116,7 +116,9 @@ def test_no_existing_pre_commit_conifg(_, default_choice, tmpdir, config): if "json" in default_choice: assert json.load(file) == EXPECTED_DICT_CONFIG elif "yaml" in default_choice: - assert yaml.load(file) == EXPECTED_DICT_CONFIG + assert ( + yaml.load(file, Loader=yaml.FullLoader) == EXPECTED_DICT_CONFIG + ) else: config_data = file.read() assert config_data == expected_config @@ -136,7 +138,9 @@ def test_empty_pre_commit_config(_, default_choice, tmpdir, config): if "json" in default_choice: assert json.load(file) == EXPECTED_DICT_CONFIG elif "yaml" in default_choice: - assert yaml.load(file) == EXPECTED_DICT_CONFIG + assert ( + yaml.load(file, Loader=yaml.FullLoader) == EXPECTED_DICT_CONFIG + ) else: config_data = file.read() assert config_data == expected_config @@ -162,7 +166,9 @@ def test_pre_commit_config_without_cz_hook(_, default_choice, tmpdir, config): if "json" in default_choice: assert json.load(file) == EXPECTED_DICT_CONFIG elif "yaml" in default_choice: - assert yaml.load(file) == EXPECTED_DICT_CONFIG + assert ( + yaml.load(file, Loader=yaml.FullLoader) == EXPECTED_DICT_CONFIG + ) else: config_data = file.read() assert config_data == expected_config @@ -184,7 +190,9 @@ def test_cz_hook_exists_in_pre_commit_config(_, default_choice, tmpdir, config): if "json" in default_choice: assert json.load(file) == EXPECTED_DICT_CONFIG elif "yaml" in default_choice: - assert yaml.load(file) == EXPECTED_DICT_CONFIG + assert ( + yaml.load(file, Loader=yaml.FullLoader) == EXPECTED_DICT_CONFIG + ) else: config_data = file.read() assert config_data == expected_config