Skip to content

Commit 53a1d17

Browse files
jenstroegerLee-W
authored andcommitted
feat: add major-version-zero option to support initial package development
1 parent cb798dc commit 53a1d17

File tree

7 files changed

+145
-4
lines changed

7 files changed

+145
-4
lines changed

commitizen/cli.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@
183183
"default": False,
184184
"help": "retry commit if it fails the 1st time",
185185
},
186+
{
187+
"name": ["--major-version-zero"],
188+
"action": "store_true",
189+
"default": None,
190+
"help": "keep major version at zero, even for breaking changes",
191+
},
186192
{
187193
"name": "manual_version",
188194
"type": str,

commitizen/commands/bump.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import questionary
66
from packaging.version import InvalidVersion, Version
77

8-
from commitizen import bump, cmd, factory, git, out
8+
from commitizen import bump, cmd, defaults, factory, git, out
99
from commitizen.commands.changelog import Changelog
1010
from commitizen.config import BaseConfig
1111
from commitizen.exceptions import (
@@ -45,6 +45,7 @@ def __init__(self, config: BaseConfig, arguments: dict):
4545
"bump_message",
4646
"gpg_sign",
4747
"annotated_tag",
48+
"major_version_zero",
4849
]
4950
if arguments[key] is not None
5051
},
@@ -101,6 +102,7 @@ def __call__(self): # noqa: C901
101102
tag_format: str = self.bump_settings["tag_format"]
102103
bump_commit_message: str = self.bump_settings["bump_message"]
103104
version_files: List[str] = self.bump_settings["version_files"]
105+
major_version_zero: bool = self.bump_settings["major_version_zero"]
104106

105107
dry_run: bool = self.arguments["dry_run"]
106108
is_yes: bool = self.arguments["yes"]
@@ -126,6 +128,20 @@ def __call__(self): # noqa: C901
126128
"--local-version cannot be combined with MANUAL_VERSION"
127129
)
128130

131+
if major_version_zero:
132+
raise NotAllowed(
133+
"--major-version-zero cannot be combined with MANUAL_VERSION"
134+
)
135+
136+
if major_version_zero:
137+
if not current_version.startswith("0."):
138+
raise NotAllowed(
139+
f"--major-version-zero is meaningless for current version {current_version}"
140+
)
141+
142+
# Update the bump map to ensure major version doesn't increment.
143+
self.cz.bump_map = defaults.bump_map_major_version_zero
144+
129145
current_tag_version: str = bump.normalize_tag(
130146
current_version, tag_format=tag_format
131147
)

commitizen/defaults.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Settings(TypedDict, total=False):
3939
use_shortcuts: bool
4040
style: Optional[List[Tuple[str, str]]]
4141
customize: CzSettings
42+
major_version_zero: bool
4243

4344

4445
name: str = "cz_conventional_commits"
@@ -63,6 +64,7 @@ class Settings(TypedDict, total=False):
6364
"changelog_start_rev": None,
6465
"update_changelog_on_bump": False,
6566
"use_shortcuts": False,
67+
"major_version_zero": False,
6668
}
6769

6870
MAJOR = "MAJOR"
@@ -80,6 +82,16 @@ class Settings(TypedDict, total=False):
8082
(r"^perf", PATCH),
8183
)
8284
)
85+
bump_map_major_version_zero = OrderedDict(
86+
(
87+
(r"^.+!$", MINOR),
88+
(r"^BREAKING[\-\ ]CHANGE", MINOR),
89+
(r"^feat", MINOR),
90+
(r"^fix", PATCH),
91+
(r"^refactor", PATCH),
92+
(r"^perf", PATCH),
93+
)
94+
)
8395
change_type_order = ["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"]
8496
bump_message = "bump: version $current_version → $new_version"
8597

docs/bump.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ 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] [MANUAL_VERSION]
62+
[--changelog-to-stdout] [--retry] [--major-version-zero]
63+
[MANUAL_VERSION]
6364

6465
positional arguments:
6566
MANUAL_VERSION bump to the given version (e.g: 1.5.3)
@@ -93,6 +94,7 @@ options:
9394
--changelog-to-stdout
9495
Output changelog to the stdout
9596
--retry retry commit if it fails the 1st time
97+
--major-version-zero keep major version at zero, even for breaking changes
9698
```
9799
98100
### `--files-only`
@@ -199,6 +201,18 @@ It will retry the commit if it fails the 1st time.
199201
200202
Useful to combine with code formatters, like [Prettier](https://prettier.io/).
201203
204+
### `--major-version-zero`
205+
206+
A project in its initial development should have a major version zero, and even breaking changes
207+
should not bump that major version from zero. This command ensures that behavior.
208+
209+
If `--major-version-zero` is used for projects that have a version number greater than zero it fails.
210+
If used together with a manual version the command also fails.
211+
212+
We recommend setting `major_version_zero = true` in your configuration file while a project
213+
is in its initial development. Remove that configuration using a breaking-change commit to bump
214+
your project’s major version to `v1.0.0` once your project has reached maturity.
215+
202216
## Avoid raising errors
203217
204218
Some situations from commitizen rise an exit code different than 0.
@@ -369,6 +383,8 @@ When set to `true` commitizen will create annotated tags.
369383
370384
```toml
371385
[tool.commitizen]
386+
annotated_tag = true
387+
```
372388
373389
---
374390
@@ -379,7 +395,20 @@ When set to `true` commitizen will create gpg signed tags.
379395
```toml
380396
[tool.commitizen]
381397
gpg_sign = true
382-
annotated_tag = true
398+
```
399+
400+
---
401+
402+
### `major_version_zero`
403+
404+
When set to `true` commitizen will keep the major version at zero.
405+
Useful during the initial development stage of your project.
406+
407+
Defaults to: `false`
408+
409+
```toml
410+
[tool.commitizen]
411+
major_version_zero = true
383412
```
384413
385414
## Custom bump

tests/commands/test_bump_command.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
NotAllowed,
2323
NoVersionSpecifiedError,
2424
)
25-
from tests.utils import create_file_and_commit
25+
from tests.utils import create_file_and_commit, create_tag
2626

2727

2828
@pytest.mark.parametrize(
@@ -151,6 +151,31 @@ def test_bump_major_increment(commit_msg, mocker):
151151
assert tag_exists is True
152152

153153

154+
@pytest.mark.usefixtures("tmp_commitizen_project")
155+
@pytest.mark.parametrize(
156+
"commit_msg",
157+
(
158+
"feat: new user interface\n\nBREAKING CHANGE: age is no longer supported",
159+
"feat!: new user interface\n\nBREAKING CHANGE: age is no longer supported",
160+
"feat!: new user interface",
161+
"feat(user): new user interface\n\nBREAKING CHANGE: age is no longer supported",
162+
"feat(user)!: new user interface\n\nBREAKING CHANGE: age is no longer supported",
163+
"feat(user)!: new user interface",
164+
"BREAKING CHANGE: age is no longer supported",
165+
"BREAKING-CHANGE: age is no longer supported",
166+
),
167+
)
168+
def test_bump_major_increment_major_version_zero(commit_msg, mocker):
169+
create_file_and_commit(commit_msg)
170+
171+
testargs = ["cz", "bump", "--yes", "--major-version-zero"]
172+
mocker.patch.object(sys, "argv", testargs)
173+
cli.main()
174+
175+
tag_exists = git.tag_exist("0.2.0")
176+
assert tag_exists is True
177+
178+
154179
@pytest.mark.usefixtures("tmp_commitizen_project")
155180
@pytest.mark.parametrize(
156181
"commit_msg,increment,expected_tag",
@@ -320,6 +345,34 @@ def test_bump_when_version_inconsistent_in_version_files(
320345
assert partial_expected_error_message in str(excinfo.value)
321346

322347

348+
def test_bump_major_version_zero_when_major_is_not_zero(mocker, tmp_commitizen_project):
349+
tmp_version_file = tmp_commitizen_project.join("__version__.py")
350+
tmp_version_file.write("1.0.0")
351+
tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml")
352+
tmp_commitizen_cfg_file.write(
353+
f"[tool.commitizen]\n"
354+
'version="1.0.0"\n'
355+
f'version_files = ["{str(tmp_version_file)}"]'
356+
)
357+
tmp_changelog_file = tmp_commitizen_project.join("CHANGELOG.md")
358+
tmp_changelog_file.write("## v1.0.0")
359+
360+
create_file_and_commit("feat(user): new file")
361+
create_tag("v1.0.0")
362+
create_file_and_commit("feat(user)!: new file")
363+
364+
testargs = ["cz", "bump", "--yes", "--major-version-zero"]
365+
mocker.patch.object(sys, "argv", testargs)
366+
367+
with pytest.raises(NotAllowed) as excinfo:
368+
cli.main()
369+
370+
expected_error_message = (
371+
"--major-version-zero is meaningless for current version 1.0.0"
372+
)
373+
assert expected_error_message in str(excinfo.value)
374+
375+
323376
def test_bump_files_only(mocker, tmp_commitizen_project):
324377
tmp_version_file = tmp_commitizen_project.join("__version__.py")
325378
tmp_version_file.write("0.1.0")
@@ -683,3 +736,20 @@ def test_bump_manual_version(mocker, manual_version):
683736
cli.main()
684737
tag_exists = git.tag_exist(manual_version)
685738
assert tag_exists is True
739+
740+
741+
@pytest.mark.usefixtures("tmp_commitizen_project")
742+
def test_bump_manual_version_disallows_major_version_zero(mocker):
743+
create_file_and_commit("feat: new file")
744+
745+
manual_version = "0.2.0"
746+
testargs = ["cz", "bump", "--yes", "--major-version-zero", manual_version]
747+
mocker.patch.object(sys, "argv", testargs)
748+
749+
with pytest.raises(NotAllowed) as excinfo:
750+
cli.main()
751+
752+
expected_error_message = (
753+
"--major-version-zero cannot be combined with MANUAL_VERSION"
754+
)
755+
assert expected_error_message in str(excinfo.value)

tests/test_conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"changelog_start_rev": None,
4949
"update_changelog_on_bump": False,
5050
"use_shortcuts": False,
51+
"major_version_zero": False,
5152
}
5253

5354
_new_settings = {
@@ -63,6 +64,7 @@
6364
"changelog_start_rev": None,
6465
"update_changelog_on_bump": False,
6566
"use_shortcuts": False,
67+
"major_version_zero": False,
6668
}
6769

6870
_read_settings = {

tests/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ def create_file_and_commit(message: str, filename: Optional[str] = None):
2626
raise exceptions.CommitError(c.err)
2727

2828

29+
def create_tag(tag: str):
30+
c = git.tag(tag)
31+
if c.return_code != 0:
32+
raise exceptions.CommitError(c.err)
33+
34+
2935
def wait_for_tag():
3036
"""Wait for tag.
3137

0 commit comments

Comments
 (0)