diff --git a/commitizen/cli.py b/commitizen/cli.py index 00be6daf15..3c529e4210 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -353,6 +353,12 @@ def __call__( "help": "Add additional build-metadata to the version-number", "default": None, }, + { + "name": ["--get-next"], + "action": "store_true", + "help": "Determine the next version and write to stdout", + "default": False, + }, ], }, { diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 8497384665..a3682df8f3 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -14,6 +14,7 @@ BumpTagFailedError, DryRunExit, ExpectedExit, + GetNextExit, InvalidManualVersion, NoCommitsFoundError, NoneIncrementExit, @@ -159,6 +160,7 @@ def __call__(self) -> None: # noqa: C901 manual_version = self.arguments["manual_version"] build_metadata = self.arguments["build_metadata"] increment_mode: str = self.arguments["increment_mode"] + get_next: bool = self.arguments["get_next"] if manual_version: if increment: @@ -190,6 +192,9 @@ def __call__(self) -> None: # noqa: C901 "--prerelease-offset cannot be combined with MANUAL_VERSION" ) + if get_next: + raise NotAllowed("--get-next cannot be combined with MANUAL_VERSION") + if major_version_zero: if not current_version.release[0] == 0: raise NotAllowed( @@ -202,6 +207,18 @@ def __call__(self) -> None: # noqa: C901 "--local-version cannot be combined with --build-metadata" ) + # If user specified changelog_to_stdout, they probably want the + # changelog to be generated as well, this is the most intuitive solution + self.changelog = self.changelog or bool(self.changelog_to_stdout) + + if get_next: + if self.changelog: + raise NotAllowed( + "--changelog or --changelog-to-stdout is not allowed with --get-next" + ) + # Setting dry_run to prevent any unwanted changes to the repo or files + self.dry_run = True + current_tag_version: str = bump.normalize_tag( current_version, tag_format=tag_format, @@ -210,10 +227,6 @@ def __call__(self) -> None: # noqa: C901 is_initial = self.is_initial_tag(current_tag_version, is_yes) - # If user specified changelog_to_stdout, they probably want the - # changelog to be generated as well, this is the most intuitive solution - self.changelog = self.changelog or bool(self.changelog_to_stdout) - if manual_version: try: new_version = self.scheme(manual_version) @@ -266,6 +279,16 @@ def __call__(self) -> None: # noqa: C901 current_version, new_version, bump_commit_message ) + if get_next: + if increment is None and new_tag_version == current_tag_version: + raise NoneIncrementExit( + "[NO_COMMITS_TO_BUMP]\n" + "The commits found are not eligible to be bumped" + ) + + out.write(str(new_version)) + raise GetNextExit() + # Report found information information = f"{message}\n" f"tag to create: {new_tag_version}\n" if increment: diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index b0fc4e382d..c96803b5fb 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -67,6 +67,10 @@ class DryRunExit(ExpectedExit): pass +class GetNextExit(ExpectedExit): + pass + + class NoneIncrementExit(CommitizenException): exit_code = ExitCode.NO_INCREMENT diff --git a/docs/commands/bump.md b/docs/commands/bump.md index 4d370a6a1f..efb5b0881d 100644 --- a/docs/commands/bump.md +++ b/docs/commands/bump.md @@ -285,6 +285,39 @@ You should normally not use this functionality, but if you decide to do, keep in * Version `1.2.3+a`, and `1.2.3+b` are the same version! Tools should not use the string after `+` for version calculation. This is probably not a guarantee (example in helm) even tho it is in the spec. * It might be problematic having the metadata in place when doing upgrades depending on what tool you use. +### `--get-next` + +Provides a way to determine the next version and write it to stdout. This parameter is not compatible with `--changelog` +and `manual version`. + +```bash +cz bump --get-next +``` + +Will output the next version, e.g., `1.2.3`. This can be useful for determining the next version based on CI for non +production environments/builds. + +This behavior differs from the `--dry-run` flag. The `--dry-run` flag provides a more detailed output and can also show +the changes as they would appear in the changelog file. + +The following output is the result of `cz bump --dry-run`: + +``` +bump: version 3.28.0 → 3.29.0 +tag to create: v3.29.0 +increment detected: MINOR +``` + +The following output is the result of `cz bump --get-next`: + +``` +3.29.0 +``` + +The `--get-next` flag will raise a `NoneIncrementExit` if the found commits are not eligible for a version bump. + +For information on how to suppress this exit, see [avoid raising errors](#avoid-raising-errors). + ## Avoid raising errors Some situations from commitizen raise an exit code different than 0. diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index a0271c322e..90bca62070 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -21,6 +21,7 @@ DryRunExit, ExitCode, ExpectedExit, + GetNextExit, InvalidManualVersion, NoCommitsFoundError, NoneIncrementExit, @@ -1462,3 +1463,63 @@ def test_bump_command_shows_description_when_use_help_option( out, _ = capsys.readouterr() file_regression.check(out, extension=".txt") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next(mocker: MockFixture, capsys): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--get-next"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(GetNextExit): + cli.main() + + out, _ = capsys.readouterr() + assert "0.2.0" in out + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is False + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__changelog_is_not_allowed(mocker: MockFixture): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--get-next", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__changelog_to_stdout_is_not_allowed(mocker: MockFixture): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--get-next", "--changelog-to-stdout"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__manual_version_is_not_allowed(mocker: MockFixture): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--get-next", "0.2.1"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__no_eligible_commits_raises(mocker: MockFixture): + create_file_and_commit("chore: new commit") + + testargs = ["cz", "bump", "--yes", "--get-next"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NoneIncrementExit): + cli.main() diff --git a/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt b/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt index de76f2efcf..7c3f8ac805 100644 --- a/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt +++ b/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt @@ -10,7 +10,7 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog] [--file-name FILE_NAME] [--prerelease-offset PRERELEASE_OFFSET] [--version-scheme {pep440,semver,semver2}] [--version-type {pep440,semver,semver2}] - [--build-metadata BUILD_METADATA] + [--build-metadata BUILD_METADATA] [--get-next] [MANUAL_VERSION] bump semantic version based on the git log @@ -77,3 +77,4 @@ options: Deprecated, use --version-scheme --build-metadata BUILD_METADATA Add additional build-metadata to the version-number + --get-next Determine the next version and write to stdout