Skip to content

Commit 941abf0

Browse files
authored
Merge pull request #712 from commitizen-tools/release/v3
Release/v3
2 parents e039f16 + 28ac04f commit 941abf0

24 files changed

+1215
-640
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,6 @@ venv.bak/
110110

111111
# macOSX
112112
.DS_Store
113+
114+
# ruff
115+
.ruff_cache

commitizen/commands/init.py

+193-31
Original file line numberDiff line numberDiff line change
@@ -13,59 +13,152 @@
1313
from commitizen.defaults import config_files
1414
from commitizen.exceptions import InitFailedError, NoAnswersError
1515
from commitizen.git import get_latest_tag_name, get_tag_names, smart_open
16+
from commitizen.version_types import VERSION_TYPES
17+
18+
19+
class ProjectInfo:
20+
"""Discover information about the current folder."""
21+
22+
@property
23+
def has_pyproject(self) -> bool:
24+
return os.path.isfile("pyproject.toml")
25+
26+
@property
27+
def has_setup(self) -> bool:
28+
return os.path.isfile("setup.py")
29+
30+
@property
31+
def has_pre_commit_config(self) -> bool:
32+
return os.path.isfile(".pre-commit-config.yaml")
33+
34+
@property
35+
def is_python_poetry(self) -> bool:
36+
if not self.has_pyproject:
37+
return False
38+
with open("pyproject.toml") as f:
39+
return "tool.poetry.version" in f.read()
40+
41+
@property
42+
def is_python(self) -> bool:
43+
return self.has_pyproject or self.has_setup
44+
45+
@property
46+
def is_rust_cargo(self) -> bool:
47+
return os.path.isfile("Cargo.toml")
48+
49+
@property
50+
def is_npm_package(self) -> bool:
51+
return os.path.isfile("package.json")
52+
53+
@property
54+
def is_php_composer(self) -> bool:
55+
return os.path.isfile("composer.json")
56+
57+
@property
58+
def latest_tag(self) -> Optional[str]:
59+
return get_latest_tag_name()
60+
61+
def tags(self) -> Optional[List]:
62+
"""Not a property, only use if necessary"""
63+
if self.latest_tag is None:
64+
return None
65+
return get_tag_names()
66+
67+
@property
68+
def is_pre_commit_installed(self) -> bool:
69+
return not shutil.which("pre-commit")
1670

1771

1872
class Init:
1973
def __init__(self, config: BaseConfig, *args):
2074
self.config: BaseConfig = config
2175
self.cz = factory.commiter_factory(self.config)
76+
self.project_info = ProjectInfo()
2277

2378
def __call__(self):
2479
if self.config.path:
2580
out.line(f"Config file {self.config.path} already exists")
2681
return
2782

28-
# No config for commitizen exist
29-
config_path = self._ask_config_path()
83+
out.info("Welcome to commitizen!\n")
84+
out.line(
85+
"Answer the questions to configure your project.\n"
86+
"For further configuration visit:\n"
87+
"\n"
88+
"https://commitizen-tools.github.io/commitizen/config/"
89+
"\n"
90+
)
91+
92+
# Collect information
93+
try:
94+
config_path = self._ask_config_path() # select
95+
cz_name = self._ask_name() # select
96+
version_provider = self._ask_version_provider() # select
97+
tag = self._ask_tag() # confirm & select
98+
version = Version(tag)
99+
tag_format = self._ask_tag_format(tag) # confirm & text
100+
version_type = self._ask_version_type() # select
101+
update_changelog_on_bump = self._ask_update_changelog_on_bump() # confirm
102+
major_version_zero = self._ask_major_version_zero(version) # confirm
103+
except KeyboardInterrupt:
104+
raise InitFailedError("Stopped by user")
105+
106+
# Initialize configuration
30107
if "toml" in config_path:
31108
self.config = TomlConfig(data="", path=config_path)
32109
elif "json" in config_path:
33110
self.config = JsonConfig(data="{}", path=config_path)
34111
elif "yaml" in config_path:
35112
self.config = YAMLConfig(data="", path=config_path)
36-
self.config.init_empty_config_content()
37-
38113
values_to_add = {}
39-
values_to_add["name"] = self._ask_name()
40-
tag = self._ask_tag()
41-
values_to_add["version"] = Version(tag).public
42-
values_to_add["tag_format"] = self._ask_tag_format(tag)
43-
self._update_config_file(values_to_add)
114+
values_to_add["name"] = cz_name
115+
values_to_add["tag_format"] = tag_format
116+
values_to_add["version_type"] = version_type
44117

118+
if version_provider == "commitizen":
119+
values_to_add["version"] = version.public
120+
else:
121+
values_to_add["version_provider"] = version_provider
122+
123+
if update_changelog_on_bump:
124+
values_to_add["update_changelog_on_bump"] = update_changelog_on_bump
125+
126+
if major_version_zero:
127+
values_to_add["major_version_zero"] = major_version_zero
128+
129+
# Collect hook data
45130
hook_types = questionary.checkbox(
46131
"What types of pre-commit hook you want to install? (Leave blank if you don't want to install)",
47132
choices=[
48-
questionary.Choice("commit-msg", checked=True),
49-
questionary.Choice("pre-push", checked=True),
133+
questionary.Choice("commit-msg", checked=False),
134+
questionary.Choice("pre-push", checked=False),
50135
],
51-
).ask()
136+
).unsafe_ask()
52137
if hook_types:
53138
try:
54139
self._install_pre_commit_hook(hook_types)
55140
except InitFailedError as e:
56141
raise InitFailedError(f"Failed to install pre-commit hook.\n{e}")
57142

58-
out.write("You can bump the version and create changelog running:\n")
59-
out.info("cz bump --changelog")
60-
out.success("The configuration are all set.")
143+
# Create and initialize config
144+
self.config.init_empty_config_content()
145+
self._update_config_file(values_to_add)
146+
147+
out.write("\nYou can bump the version running:\n")
148+
out.info("\tcz bump\n")
149+
out.success("Configuration complete 🚀")
61150

62151
def _ask_config_path(self) -> str:
152+
default_path = ".cz.toml"
153+
if self.project_info.has_pyproject:
154+
default_path = "pyproject.toml"
155+
63156
name: str = questionary.select(
64-
"Please choose a supported config file: (default: pyproject.toml)",
157+
"Please choose a supported config file: ",
65158
choices=config_files,
66-
default="pyproject.toml",
159+
default=default_path,
67160
style=self.cz.style,
68-
).ask()
161+
).unsafe_ask()
69162
return name
70163

71164
def _ask_name(self) -> str:
@@ -74,29 +167,29 @@ def _ask_name(self) -> str:
74167
choices=list(registry.keys()),
75168
default="cz_conventional_commits",
76169
style=self.cz.style,
77-
).ask()
170+
).unsafe_ask()
78171
return name
79172

80173
def _ask_tag(self) -> str:
81-
latest_tag = get_latest_tag_name()
174+
latest_tag = self.project_info.latest_tag
82175
if not latest_tag:
83176
out.error("No Existing Tag. Set tag to v0.0.1")
84177
return "0.0.1"
85178

86179
is_correct_tag = questionary.confirm(
87180
f"Is {latest_tag} the latest tag?", style=self.cz.style, default=False
88-
).ask()
181+
).unsafe_ask()
89182
if not is_correct_tag:
90-
tags = get_tag_names()
183+
tags = self.project_info.tags()
91184
if not tags:
92185
out.error("No Existing Tag. Set tag to v0.0.1")
93186
return "0.0.1"
94187

95188
latest_tag = questionary.select(
96189
"Please choose the latest tag: ",
97-
choices=get_tag_names(), # type: ignore
190+
choices=tags,
98191
style=self.cz.style,
99-
).ask()
192+
).unsafe_ask()
100193

101194
if not latest_tag:
102195
raise NoAnswersError("Tag is required!")
@@ -108,21 +201,90 @@ def _ask_tag_format(self, latest_tag) -> str:
108201
tag_format = r"v$version"
109202
is_correct_format = questionary.confirm(
110203
f'Is "{tag_format}" the correct tag format?', style=self.cz.style
111-
).ask()
204+
).unsafe_ask()
112205

113206
if not is_correct_format:
114207
tag_format = questionary.text(
115208
'Please enter the correct version format: (default: "$version")',
116209
style=self.cz.style,
117-
).ask()
210+
).unsafe_ask()
118211

119212
if not tag_format:
120213
tag_format = "$version"
121214
return tag_format
122215

123-
def _search_pre_commit(self) -> bool:
124-
"""Check whether pre-commit is installed"""
125-
return shutil.which("pre-commit") is not None
216+
def _ask_version_provider(self) -> str:
217+
"""Ask for setting: version_provider"""
218+
219+
OPTS = {
220+
"commitizen": "commitizen: Fetch and set version in commitizen config (default)",
221+
"cargo": "cargo: Get and set version from Cargo.toml:project.version field",
222+
"composer": "composer: Get and set version from composer.json:project.version field",
223+
"npm": "npm: Get and set version from package.json:project.version field",
224+
"pep621": "pep621: Get and set version from pyproject.toml:project.version field",
225+
"poetry": "poetry: Get and set version from pyproject.toml:tool.poetry.version field",
226+
"scm": "scm: Fetch the version from git and does not need to set it back",
227+
}
228+
229+
default_val = "commitizen"
230+
if self.project_info.is_python:
231+
if self.project_info.is_python_poetry:
232+
default_val = "poetry"
233+
else:
234+
default_val = "pep621"
235+
elif self.project_info.is_rust_cargo:
236+
default_val = "cargo"
237+
elif self.project_info.is_npm_package:
238+
default_val = "npm"
239+
elif self.project_info.is_php_composer:
240+
default_val = "composer"
241+
242+
choices = [
243+
questionary.Choice(title=title, value=value)
244+
for value, title in OPTS.items()
245+
]
246+
default = next(filter(lambda x: x.value == default_val, choices))
247+
version_provider: str = questionary.select(
248+
"Choose the source of the version:",
249+
choices=choices,
250+
style=self.cz.style,
251+
default=default,
252+
).unsafe_ask()
253+
return version_provider
254+
255+
def _ask_version_type(self) -> str:
256+
"""Ask for setting: version_type"""
257+
default = "semver"
258+
if self.project_info.is_python:
259+
default = "pep440"
260+
261+
version_type: str = questionary.select(
262+
"Choose version type scheme: ",
263+
choices=[*VERSION_TYPES],
264+
style=self.cz.style,
265+
default=default,
266+
).unsafe_ask()
267+
return version_type
268+
269+
def _ask_major_version_zero(self, version: Version) -> bool:
270+
"""Ask for setting: major_version_zero"""
271+
if version.major > 0:
272+
return False
273+
major_version_zero: bool = questionary.confirm(
274+
"Keep major version zero (0.x) during breaking changes",
275+
default=True,
276+
auto_enter=True,
277+
).unsafe_ask()
278+
return major_version_zero
279+
280+
def _ask_update_changelog_on_bump(self) -> bool:
281+
"Ask for setting: update_changelog_on_bump"
282+
update_changelog_on_bump: bool = questionary.confirm(
283+
"Create changelog automatically on bump",
284+
default=True,
285+
auto_enter=True,
286+
).unsafe_ask()
287+
return update_changelog_on_bump
126288

127289
def _exec_install_pre_commit_hook(self, hook_types: List[str]):
128290
cmd_str = self._gen_pre_commit_cmd(hook_types)
@@ -157,7 +319,7 @@ def _install_pre_commit_hook(self, hook_types: Optional[List[str]] = None):
157319
}
158320

159321
config_data = {}
160-
if not os.path.isfile(pre_commit_config_filename):
322+
if not self.project_info.has_pre_commit_config:
161323
# .pre-commit-config.yaml does not exist
162324
config_data["repos"] = [cz_hook_config]
163325
else:
@@ -180,7 +342,7 @@ def _install_pre_commit_hook(self, hook_types: Optional[List[str]] = None):
180342
with smart_open(pre_commit_config_filename, "w") as config_file:
181343
yaml.safe_dump(config_data, stream=config_file)
182344

183-
if not self._search_pre_commit():
345+
if not self.project_info.is_pre_commit_installed:
184346
raise InitFailedError("pre-commit is not installed in current environment.")
185347
if hook_types is None:
186348
hook_types = ["commit-msg", "pre-push"]

commitizen/version_types.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,6 @@ def __str__(self) -> str:
9494

9595

9696
VERSION_TYPES = {
97-
"pep440": Version,
9897
"semver": SemVerVersion,
98+
"pep440": Version,
9999
}

0 commit comments

Comments
 (0)