Skip to content

Commit 2ba0b73

Browse files
committed
feat(bump): support optional manual version argument
1 parent 230aecf commit 2ba0b73

File tree

5 files changed

+140
-34
lines changed

5 files changed

+140
-34
lines changed

commitizen/cli.py

+7
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,13 @@
183183
"default": False,
184184
"help": "retry commit if it fails the 1st time",
185185
},
186+
{
187+
"name": "manual_version",
188+
"type": str,
189+
"nargs": "?",
190+
"help": "bump to the given version (e.g: 1.5.3)",
191+
"metavar": "MANUAL_VERSION",
192+
},
186193
],
187194
},
188195
{

commitizen/commands/bump.py

+58-33
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import List, Optional
33

44
import questionary
5-
from packaging.version import Version
5+
from packaging.version import InvalidVersion, Version
66

77
from commitizen import bump, cmd, factory, git, out
88
from commitizen.commands.changelog import Changelog
@@ -12,10 +12,12 @@
1212
BumpTagFailedError,
1313
DryRunExit,
1414
ExpectedExit,
15+
InvalidManualVersion,
1516
NoCommitsFoundError,
1617
NoneIncrementExit,
1718
NoPatternMapError,
1819
NotAGitProjectError,
20+
NotAllowed,
1921
NoVersionSpecifiedError,
2022
)
2123

@@ -106,6 +108,22 @@ def __call__(self): # noqa: C901
106108
devrelease: Optional[int] = self.arguments["devrelease"]
107109
is_files_only: Optional[bool] = self.arguments["files_only"]
108110
is_local_version: Optional[bool] = self.arguments["local_version"]
111+
manual_version = self.arguments["manual_version"]
112+
113+
if manual_version:
114+
if increment:
115+
raise NotAllowed("--increment cannot be combined with MANUAL_VERSION")
116+
117+
if prerelease:
118+
raise NotAllowed("--prerelease cannot be combined with MANUAL_VERSION")
119+
120+
if devrelease is not None:
121+
raise NotAllowed("--devrelease cannot be combined with MANUAL_VERSION")
122+
123+
if is_local_version:
124+
raise NotAllowed(
125+
"--local-version cannot be combined with MANUAL_VERSION"
126+
)
109127

110128
current_tag_version: str = bump.normalize_tag(
111129
current_version, tag_format=tag_format
@@ -127,46 +145,53 @@ def __call__(self): # noqa: C901
127145
if not commits and not current_version_instance.is_prerelease:
128146
raise NoCommitsFoundError("[NO_COMMITS_FOUND]\n" "No new commits found.")
129147

130-
if increment is None:
131-
increment = self.find_increment(commits)
132-
133-
# It may happen that there are commits, but they are not eligible
134-
# for an increment, this generates a problem when using prerelease (#281)
135-
if (
136-
prerelease
137-
and increment is None
138-
and not current_version_instance.is_prerelease
139-
):
140-
raise NoCommitsFoundError(
141-
"[NO_COMMITS_FOUND]\n"
142-
"No commits found to generate a pre-release.\n"
143-
"To avoid this error, manually specify the type of increment with `--increment`"
144-
)
145-
146-
# Increment is removed when current and next version
147-
# are expected to be prereleases.
148-
if prerelease and current_version_instance.is_prerelease:
149-
increment = None
148+
if manual_version:
149+
try:
150+
new_version = Version(manual_version)
151+
except InvalidVersion as exc:
152+
raise InvalidManualVersion(
153+
"[INVALID_MANUAL_VERSION]\n"
154+
f"Invalid manual version: '{manual_version}'"
155+
) from exc
156+
else:
157+
if increment is None:
158+
increment = self.find_increment(commits)
159+
160+
# It may happen that there are commits, but they are not eligible
161+
# for an increment, this generates a problem when using prerelease (#281)
162+
if (
163+
prerelease
164+
and increment is None
165+
and not current_version_instance.is_prerelease
166+
):
167+
raise NoCommitsFoundError(
168+
"[NO_COMMITS_FOUND]\n"
169+
"No commits found to generate a pre-release.\n"
170+
"To avoid this error, manually specify the type of increment with `--increment`"
171+
)
150172

151-
new_version = bump.generate_version(
152-
current_version,
153-
increment,
154-
prerelease=prerelease,
155-
devrelease=devrelease,
156-
is_local_version=is_local_version,
157-
)
173+
# Increment is removed when current and next version
174+
# are expected to be prereleases.
175+
if prerelease and current_version_instance.is_prerelease:
176+
increment = None
177+
178+
new_version = bump.generate_version(
179+
current_version,
180+
increment,
181+
prerelease=prerelease,
182+
devrelease=devrelease,
183+
is_local_version=is_local_version,
184+
)
158185

159186
new_tag_version = bump.normalize_tag(new_version, tag_format=tag_format)
160187
message = bump.create_commit_message(
161188
current_version, new_version, bump_commit_message
162189
)
163190

164191
# Report found information
165-
information = (
166-
f"{message}\n"
167-
f"tag to create: {new_tag_version}\n"
168-
f"increment detected: {increment}\n"
169-
)
192+
information = f"{message}\n" f"tag to create: {new_tag_version}\n"
193+
if increment:
194+
information += f"increment detected: {increment}\n"
170195

171196
if self.changelog_to_stdout:
172197
# When the changelog goes to stdout, we want to send

commitizen/exceptions.py

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class ExitCode(enum.IntEnum):
2828
NO_INCREMENT = 21
2929
UNRECOGNIZED_CHARACTERSET_ENCODING = 22
3030
GIT_COMMAND_ERROR = 23
31+
INVALID_MANUAL_VERSION = 24
3132

3233

3334
class CommitizenException(Exception):
@@ -158,3 +159,7 @@ class CharacterSetDecodeError(CommitizenException):
158159

159160
class GitCommandError(CommitizenException):
160161
exit_code = ExitCode.GIT_COMMAND_ERROR
162+
163+
164+
class InvalidManualVersion(CommitizenException):
165+
exit_code = ExitCode.INVALID_MANUAL_VERSION

docs/bump.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog]
5959
[--bump-message BUMP_MESSAGE] [--prerelease {alpha,beta,rc}]
6060
[--devrelease DEVRELEASE] [--increment {MAJOR,MINOR,PATCH}]
6161
[--check-consistency] [--annotated-tag] [--gpg-sign]
62-
[--changelog-to-stdout] [--retry]
62+
[--changelog-to-stdout] [--retry] [MANUAL_VERSION]
63+
64+
positional arguments:
65+
MANUAL_VERSION bump to the given version (e.g: 1.5.3)
6366

6467
options:
6568
-h, --help show this help message and exit

tests/commands/test_bump_command.py

+66
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
DryRunExit,
1515
ExitCode,
1616
ExpectedExit,
17+
InvalidManualVersion,
1718
NoCommitsFoundError,
1819
NoneIncrementExit,
1920
NoPatternMapError,
2021
NotAGitProjectError,
22+
NotAllowed,
2123
NoVersionSpecifiedError,
2224
)
2325
from tests.utils import create_file_and_commit
@@ -614,3 +616,67 @@ def test_bump_changelog_command_commits_untracked_changelog_and_version_files(
614616
commit_file_names = git.get_filenames_in_commit()
615617
assert "CHANGELOG.md" in commit_file_names
616618
assert version_filepath in commit_file_names
619+
620+
621+
@pytest.mark.parametrize(
622+
"testargs",
623+
[
624+
["cz", "bump", "--local-version", "1.2.3"],
625+
["cz", "bump", "--prerelease", "rc", "1.2.3"],
626+
["cz", "bump", "--devrelease", "0", "1.2.3"],
627+
["cz", "bump", "--devrelease", "1", "1.2.3"],
628+
["cz", "bump", "--increment", "PATCH", "1.2.3"],
629+
],
630+
)
631+
def test_bump_invalid_manual_args_raises_exception(mocker, testargs):
632+
mocker.patch.object(sys, "argv", testargs)
633+
634+
with pytest.raises(NotAllowed):
635+
cli.main()
636+
637+
638+
@pytest.mark.usefixtures("tmp_commitizen_project")
639+
@pytest.mark.parametrize(
640+
"manual_version",
641+
[
642+
"noversion",
643+
"1.2..3",
644+
],
645+
)
646+
def test_bump_invalid_manual_version_raises_exception(mocker, manual_version):
647+
create_file_and_commit("feat: new file")
648+
649+
testargs = ["cz", "bump", "--yes", manual_version]
650+
mocker.patch.object(sys, "argv", testargs)
651+
652+
with pytest.raises(InvalidManualVersion) as excinfo:
653+
cli.main()
654+
655+
expected_error_message = (
656+
"[INVALID_MANUAL_VERSION]\n" f"Invalid manual version: '{manual_version}'"
657+
)
658+
assert expected_error_message in str(excinfo.value)
659+
660+
661+
@pytest.mark.usefixtures("tmp_commitizen_project")
662+
@pytest.mark.parametrize(
663+
"manual_version",
664+
[
665+
"0.0.1",
666+
"0.1.0rc2",
667+
"0.1.0.dev2",
668+
"0.1.0+1.0.0",
669+
"0.1.0rc2.dev2+1.0.0",
670+
"0.1.1",
671+
"0.2.0",
672+
"1.0.0",
673+
],
674+
)
675+
def test_bump_manual_version(mocker, manual_version):
676+
create_file_and_commit("feat: new file")
677+
678+
testargs = ["cz", "bump", "--yes", manual_version]
679+
mocker.patch.object(sys, "argv", testargs)
680+
cli.main()
681+
tag_exists = git.tag_exist(manual_version)
682+
assert tag_exists is True

0 commit comments

Comments
 (0)